home *** CD-ROM | disk | FTP | other *** search
/ Developer CD Series 1996 May: Tool Chest / Developer CD Series May 1996 (Tool Chest) (Apple Computer) (1996).iso / Sample Code / Macintosh Sample Code / SC.024.SoundApp / SoundApp.p < prev    next >
Encoding:
Text File  |  1994-11-18  |  157.6 KB  |  4,098 lines  |  [TEXT/MPS ]

  1. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2. #
  3. # Apple Macintosh Developer Technical Support
  4. #
  5. # MultiFinder-Aware SoundApp Application
  6. #
  7. # SoundApp
  8. #
  9. # SoundApp.p - MPW 3.0/3.1 Pascal Source
  10. #
  11. # Jim Reekes - Macintosh Developer Technical Support
  12. # Copyright © 1989-1990 Apple Computer, Inc.
  13. # All rights reserved.
  14. #
  15. # Versions:
  16. #             1.03                    May, 1990
  17. #            1.04                    Sept, 1990
  18. #            1.1b1                    Nov, 1990            MPW 3.2 update
  19. #
  20. # Components:
  21. #             SoundApp.make        May, 1990            MPW build script
  22. #             SoundApp.p            May, 1990            Pascal source code
  23. #             SoundApp.r            May, 1990            Rez source code
  24. #             SoundAppSnds.r        May, 1990            Rez source code
  25. #             SoundUnit.p            May, 1990            Pascal source code
  26. #             Utilities.p            Nov, 1990            helpful routines from MacDTS
  27. #
  28. # Version comments
  29. #
  30. #    1.04: This was an update to support the Utilities Unit that MacDTS
  31. #    developed.  Some of it was routines formally created for SoundApp,
  32. #    with additional ones coming from other sources.  This helps to reduce
  33. #    the "destractive" code from the samples and makes them easier to maintain.
  34. #
  35. #    1.1:  This is the "new" SoundApp which adds some new features.
  36. #            • Some knowledge of the new Sound Manager is present
  37. #              in areas that were work-arounds for old Sound Manager bugs
  38. #            • Recording of sounds is now possible with the Sound Input Manager
  39. #            • The document window will keep the Record button hidden if
  40. #              the Sound Inpute Manager is not available for recording,
  41. #              otherwise it will expand the window size to show this button.
  42. #            • Supports copy and pasting of sound resources
  43. #            • Conversion to MPW 3.2 was established (with some amount of pain)
  44. #            • SoundApp now has its own documents
  45. #            • Supports launching by open documents from Finder
  46. #            • Added color icons for the System 7.0 desktop
  47. #            • Added the new Sound Manager error strings
  48. #            • DoErrorSound tests for safe beeps
  49. #            • Made Apple Legal happy by getting rid of the xxxxCmd
  50. #              and changed it to the freqDurationCmd.  Also got rid of
  51. #              any other use of the work "xxxx" in a musical context
  52. #
  53. #
  54. # Formatting was done with FONT = Monaco, SIZE = 9, TABS = 3
  55. #
  56. # many thanks to: Bo3b Johnson, Mark Bennett, Andy Shebanow, Keith
  57. # Rollin, Chris Derossi, Pete Helme, Darin Adler, and my co-workers that
  58. # sat near me while I was making lots of noise testing this application.
  59. #
  60. # To the reader,
  61. #
  62. # SoundApp.p is a sample application source file for demonstrating
  63. # the Sound Manager.  It requires the use of the SoundUnit to handle
  64. # all of the sound routines.  This portion of the source code handles the
  65. # application’s management of memory, errors, user interface, etc..
  66. # The read me notes are here in the source code because people tend to
  67. # not read them otherwise.
  68. #
  69. # SoundApp is a demonstration of the Sound Manager released in System
  70. # 6.0.x.  SoundApp is also a example of how to write a Macintosh
  71. # application that performs good user interface and proper error
  72. # handling.  Believe it or not, but the Sound Manager portion was the
  73. # easiest part for me to write.  It was all the user interface and error
  74. # handling that was the most difficult.  One thing became very clear to
  75. # me in the course of writing this application.  The following axiom is
  76. # one of the few great truths in the universe.
  77. #
  78. # If you write a Macintosh application without MacApp, you’re working
  79. # too hard.
  80. #
  81. # Throughout the development of SoundApp I would run into typical
  82. # situations that make programming a Macintosh too hard.  When this
  83. # happened I asked myself “what would MacApp do?” and that was followed
  84. # by the thought “then why am I writing code that is already out there
  85. # for me to use?”  I started out intending on writing a very simple
  86. # application that anyone could read, and understand the Sound Manager.
  87. # I felt this meant not requiring the person to read Object Oriented
  88. # Pascal.  I accomplished part of my goal.  People should be able to
  89. # learn how to use the Sound Manager (in its present condition), but it
  90. # didn’t turn out to be such a simple application as I had hoped for.
  91. #
  92. # I have put a large amount of comments into the code.  This is
  93. # something I’m really picky about.  People do not comment their code
  94. # enough.  Each procedure has a paragraph that should explain what to
  95. # expect that routine to do, and how it goes about doing it.  There are
  96. # some bigger issues that I will put into the release notes below.
  97. # There are some things that make the Macintosh harder to develop for
  98. # than it should be.  Simple things should be simple.  Too many things
  99. # on the Mac that should be simple are not.  Maybe someday these things
  100. # will be fixed.
  101. #
  102. # • GetFontInfo requires a port set to the font in question.  If I
  103. #   wanted to find the height of the System Font, I had to first set the
  104. #   current port to the WindowMgr.  I could have used my own window, but
  105. #   what if I needed the font info before I had a window available?
  106. # • The toolbox blows chunks when your heap gets “too low.”  I believe
  107. #   this magical number is between 32k and zero.  The odds of blowing
  108. #   chunks increase logarithmically as one approaches 0 free bytes.
  109. # • The Dialog Manager is not a free lunch and in fact will cost you
  110. #   plenty.
  111. # • There’s no safe way to determine how much memory opening a
  112. #   resource file will take away from your heap.
  113. # • There’s no way to determine how safe it is opening a resource that
  114. #   could be shared by other applications, especially on a local volume.
  115. # • The Resource Manager doesn’t always set ResErr.
  116. # • The Sound Manager returns even less errors.
  117. # • The List Manager returns no errors.
  118. # • Writing a staggering routine for new windows encompasses a number
  119. #   of difficulties.  How does one find the height of a window’s title
  120. #   region before the window is visible?
  121. #
  122. # Am I just a complainer?  Do I have a bad attitude?  Probably, but
  123. # I’m just trying to point out the above areas make the Mac programming
  124. # experience difficult.  These are areas that get developers into
  125. # trouble.  These areas need a sign in front of them that says,
  126. # “Danger!”  These are areas that developers get into and then write to
  127. # MacDTS for help.
  128. #
  129. # Notation Conventions
  130. # --------------------
  131. # All Pascal reserved words are in upper case.  All global variables
  132. # begin with a lower case “g”. All constants begin with “k” except for
  133. # those noted here.  Resource IDs begin with “r”.  Menu IDs being with
  134. # “m” and items with an “i”.  Resource strings begin with “s”.
  135. #
  136. # Human Interface
  137. # ---------------
  138. # This is the most important and about the most difficult aspect of
  139. # programming on the Macintosh.  In the development of SoundApp I gave much
  140. # thought to the human interface issues.  In fact, in talking with the Human
  141. # Interface Group additions to the existing guidelines were made.  The
  142. # method of window stacking used here was a new one.  This was documented in
  143. # a Human Interface Tech Note.  I even made one compromise (hard to
  144. # believe!) suggested by the Human Interface Group.  I originally had the
  145. # buttons and the list in my choice of font and size.  They felt that
  146. # buttons should be in the System font and the list should also be the same.
  147. # I liked my font choice better, but the group had a point that I really
  148. # couldn’t argue with.  That was, “If there isn’t a compelling reason to
  149. # change something standard, then don’t change it.”  Buttons on a Macintosh
  150. # typically appear in the System font.  Changing the font, just because I
  151. # wanted to, was considered gratuitous.  Standard File is in the System font
  152. # and it also contains a list and buttons.  Since my window are very similar
  153. # to that dialog, I’m using the System Font.
  154. #
  155. # SoundApp is never modal unless an error occurs and I need to show and
  156. # alert.  Controls are inactivated for inactive windows.  The default button
  157. # is given the proper outline, and this outline disappears when the window
  158. # is deactivated.  Keyboard equivalents for the buttons cause the button to
  159. # appear as if it had been clicked in.  The check box in the Standard File
  160. # dialog remembers the user’s last setting.  The about box is only
  161. # semi-modal.  It will allow the user access to switch to another application.
  162. # The status window under some circumstances was found to disappear too quickly,
  163. # so a built in delay was added.  Windows are centered or stacked according
  164. # to the Human Interface Guidelines.  The sound level isn’t adjusted by the
  165. # application, and instead the users is informed of the current level and
  166. # told how it can be adjusted.
  167. #
  168. # The About box
  169. # -------------
  170. # It’s rad.  Has a color icon, shows the 'vers' resource, goes Moof!™.  It
  171. # also demonstrates how to handle a modal window without the Dialog Manager.
  172. # This technique can be use for any window, including dialog windows.  The
  173. # dialog’s update routine would call UpdateDialog.  The really new point to
  174. # notice is this window is modal but ONLY within the application’s layer.
  175. # While running under MultiFinder, the user can switch to other
  176. # applications.  While the About window is present, it is the only window
  177. # belonging to the application that accepts user actions.
  178. #
  179. # Memory Management
  180. # ----------------
  181. # This has to be the most difficult portion of a Mac application to
  182. # write.  This along with the user interface.  I spent too many nights
  183. # chasing down crashes while running the application under low memory.
  184. # I found unpleasant surprises while doing this.  The Sound Manager
  185. # doesn’t check for NIL pointers.  OpenResFile may take large portions
  186. # of my heap away.  The toolbox seems to need at least 32k of free space
  187. # in the heap of my Mac II running color.
  188. #
  189. # I wrote a simple grow zone procedure that will dump a reserve memory
  190. # block.  This is only considered for use in an emergency.  I never rely
  191. # on using it directly.  If the reserve has been released, I will not
  192. # continue an operation such as playing a sound or showing the status
  193. # window until it is regained.  Grow zones should not be considered a
  194. # solution to memory management.  They can be used to augment your
  195. # overall memory management scheme.
  196. #
  197. # Error Checking
  198. # --------------
  199. # Lots and lots and lots of it.  I could even do more.  Programmers
  200. # need to do more of this.  The Sound Manager will crash when
  201. # encountering a NIL pointer.  My application should never crash.  If
  202. # you can find a way to crash this application, then I want to hear
  203. # about it.  Using a bogus 'snd ' resources doesn’t count and I’ve found
  204. # many of those.  Writing proper error checking into the code during
  205. # development really helped.  Always handle errors, and pass along the
  206. # error.  I will let the first error encountered to be passed all the
  207. # way up to the caller and eventually my error dialog will show up for
  208. # the user.
  209. #
  210. # SetPort Strategy
  211. # ----------------
  212. # Any routine that needs to use Quickdraw will set the port.  I do not
  213. # believe that it should also be responsible for restoring the port back
  214. # to what it may have been before the routine was called.  So, you’ll
  215. # find there is an absence of the GetPort, SetPort, do my thing, and
  216. # then SetPort again.  Instead I SetPort and do my thing.  The Mac often
  217. # is setting the port unnecessarily.
  218. #
  219. # Strings
  220. # -------
  221. # All of my strings are resources.  There are no strings that appear
  222. # within the code.  This helps memory management and allows me to adjust
  223. # the application to international systems without compiling any code.
  224. # I could simply use ResEdit, or some other tool, to localize this
  225. # application.  I find it is just as easy to run Rez again than
  226. # attempting to use ResEdit.  Besides, after editing with ResEdit I want
  227. # the source for that and would have to run DeRez which isn’t nearly as
  228. # clean as my original source files.
  229. #
  230. # Window Stacking
  231. # ---------------
  232. # I hate applications that will open a new document that covers up an
  233. # existing document, unless the new document covers the entire screen.
  234. # So, my application’s documents have a small window size.  I wanted to
  235. # open new windows that would not cover up older ones.  This is nice for
  236. # the user, since they will not have to move windows just to get at
  237. # other documents.  ResEdit will stagger new windows off of the
  238. # frontmost window but I find that this isn’t the best approach.  It
  239. # will still cover up other windows and I also don’t like windows that
  240. # will open half way between two monitors.  I wanted a better approach:
  241. # one that would always stagger new windows and not cover up older ones.
  242. #
  243. # When I want to center a window, I need to know its entire rectangle
  244. # size.  The rub is that I cannot determine its size until I show it
  245. # because I only know about the window’s boundsRect.  This does not
  246. # include the area that contains the title bar.  That’s the strucRgn,
  247. # which is an empty region for an invisible window.  I could do what
  248. # MacApp does, but if I have to do another thing that MacApp already
  249. # does I’ll give up and stick with MacApp.  I ended up writing a routine
  250. # that takes a guess at the height of the window’s title bar.  This is
  251. # another thing that was harder than it should have been.
  252. #
  253. # Dialog Manager (and some of the reasons I don’t like it)
  254. # --------------------------------------------------------
  255. # My first approach was to use modeless dialogs for document windows,
  256. # thinking that I could write an application that would demonstrate how to
  257. # deal with them and all of their idiosyncrasies.  Not long into the
  258. # development cycle it became obvious to me that I was fighting something
  259. # that contained more disadvantages than advantages.  I removed all the
  260. # dialog code and only used standard windows and controls the old fashion
  261. # way.  In the case of the About window, which is semi-modal, I have a test
  262. # that will return TRUE for a window that should be treated as modal.  This
  263. # allows my window to be handled by my standard event handlers and I don’t
  264. # have to write dialog filters.  There are some things that do not get
  265. # handled properly while calling ModalDialog.  ModalDialog ignores disk
  266. # insert events.  The activate or update events do not get handled for
  267. # background windows.  Using a modeless dialog fails with MultiFinder if
  268. # switching takes place while the dialog is the frontmost window.  The
  269. # problem is that DialogSelect ignores and removes the suspend/resume event.
  270. # Another advantage to all this is that drawing was much faster.
  271. #
  272. # As an example of some of the problems with ModalDialog and the activate
  273. # event.  Try this with the Finder.  Open a window and choose “View by name.”
  274. # Then select a few names with the shift key and resize the window so the
  275. # vertical scroll bar is visible.  Move this window to one edge of the
  276. # screen or a second monitor.  Now choose “Set Startup.”  This is a modal
  277. # dialog.  If you look at the Finder window with the selected files, you’ll
  278. # notice that the scrollbar and the text are still highlited.  This is not
  279. # the proper user interface.  This is because the deactivate routines are
  280. # not called while in ModalDialog.  You can even find this problem with
  281. # SoundApp.  On deactivate events I will change my controls to the inactive
  282. # state.  If you place the buttons to the side of the screen and then bring
  283. # up the standard file dialog, you’ll notice that the buttons don’t change
  284. # properly.  ModalDialog also prevents the application from updating
  285. # background windows too.  To solve this a dialog filter procedure is
  286. # required.  In most cases, this filter would be as complex and the event
  287. # loop.  It would also make it necessary to call your event routines from
  288. # outside of the normal event loop.  All on this isn’t worth the effort.
  289. #
  290. # You can see how this does not happen while using this application’s
  291. # About window.  Select an item in the document window and choose “Play
  292. # Melody.”  This will leave the status window on screen so that you can
  293. # drag it to cover the document window.  Now select “About SoundApp” to
  294. # bring up the about window.  This causes the status window to close,
  295. # which uncovers the document window leaving an invalid area.  The
  296. # document window gets an activate event, then the About window appears.
  297. # Then the document window is properly deactivated and updated.  Yeah,
  298. # just like it should happen.
  299. #
  300. # So, the tradeoff was that I didn’t have to work around all the
  301. # strange things the Dialog Manager does such as running a secondary
  302. # event loop, and requiring me to have userItems or filterProcs.  This
  303. # made the code smaller, more readable, and faster.  I think I will
  304. # avoid the Dialog Manger from now one unless I’m using a very simple
  305. # dialog.  The about window of this application proved too much for the
  306. # Dialog Manager.
  307. #
  308. # One thing dialogs are good for is running ResEdit and laying out the
  309. # dialog.  To help position controls, I used a DLOG resource of the same
  310. # size as my WIND resource.  The DITL of this dialog contains the
  311. # positions I wanted for my CNTL resources.  This helped me to look at
  312. # where I could expect my buttons to show up.  This is one of the main
  313. # reasons people think they need the Dialog Manager, because ResEdit
  314. # makes it easy to build dialogs.  ResEdit alone has contributed to
  315. # nearly all of the Dialog Manager abuse in the world today.
  316. #
  317. # I used a Rect resource for positioning the list rectangle of the
  318. # document windows.  These windows look very much like a modeless
  319. # dialog.  (They used to be, but that presented to many problems.)  The
  320. # About window is also a standard window, but shown modally.  Just like
  321. # ModalDialog, but my modal window does allow switching under
  322. # MultiFinder.  You can change the window to a dBoxProc and then
  323. # MultiFinder will not switch while this is the active window.  To help
  324. # with the layout of the about window, I position the text within it
  325. # based on the size of the window.  The status window does this too.
  326. # These two things, the Rect resource and text based on the size of the
  327. # window, help when changing the text.  If the new text doesn’t fit,
  328. # then resize the window’s resource.  I used some trick with Rez to help
  329. # layout my window contents.  Refer to the SoundApp.r sources.
  330. #
  331. # I’ve read and understood Tech Note #203, and have learned how to
  332. # apply it.  Bo3b Johnson is a smart guy, and developers should trust
  333. # his opinions.
  334. #
  335. # List Manager
  336. # ------------
  337. # It’s very easy to be tempted by this part of the toolbox, along with
  338. # the Dialog Manager.  The List Manager is a slow beast at times.  It
  339. # also has some problems with “doing the right thing.”  I’ve found that
  340. # the list will not be updated properly when the user clicks in a cell
  341. # that is out of bounds.  LClick will return TRUE with a cell that
  342. # doesn’t exists.  LActivate will erase the scrollbars instead of
  343. # highlighting the properly.  Finally, the List Manager does not return
  344. # errors.  How would a person know if LSetCell worked?
  345. #
  346. # I’ve read and understood Tech Note #203, and have learned how to
  347. # apply it.
  348. #
  349. # Resource Manager
  350. # ----------------
  351. # I test all the handles being returned from the Resource Manager
  352. # before using them, and if I get a NIL then I look at ResError.
  353. # ResError sometimes lies and returns noErr and a NIL handle.  ResError
  354. # is usually good for getting an error code AFTER you’ve already found
  355. # an error.
  356. #
  357. # Opening a resource file that is already open by another application
  358. # is dangerous.  The Resource Manager will not tell you when you’ve done
  359. # this.  There needs to be a OpenRFPerm that will return permission
  360. # errors such as resFileBusyErr.  Refer to Tech Note #185.
  361. #
  362. # When I or the Toolbox needs to get at one of my resources,
  363. # CurResFile must be set to my application.  Also, look out for one
  364. # particularly nasty situation when switching resource files.   If the
  365. # segment loader goes for a CODE segment, it better be from our resource
  366. # file!  The idea here is, in case you didn’t get it already, always
  367. # have the current resource file be set to the application.  If a
  368. # resource is needed from another file, switch momentarily to get the
  369. # resource and immediately restore the current resource file to the
  370. # application.  I take an added measure of defense and whenever I need a
  371. # resource I use the Get1Resource calls.  These will only search the
  372. # current resource file.
  373. #
  374. # Strategies For Sound
  375. # --------------------
  376. # All of the Sound Manager code is contained in the SoundUnit.p.  This
  377. # unit was written to be general purpose, providing useful routines for
  378. # other applications.  Lots of error checking is performed.  I’ve also
  379. # extended the support for SndPlay and made it really asynchronous.
  380. # I’ve demonstrated most of the abilities the present Sound Manager has
  381. # to offer.  I will have to revise the SoundAppUnit to include any new
  382. # features (e.g., multi channel support) when the next Sound Manager is
  383. # released.
  384. #
  385. # I allocate my own memory to be used as sound channels.  I allocate
  386. # these pointers early in the application’s startup time to avoid memory
  387. # fragmentation.  These channels are of the standard size (holding 128
  388. # commands) but I’ve extended the structure to include my own
  389. # information.  When I create a new sound channel, I pass it a pointer
  390. # to this memory.  This will link in the 'snth' resource and hardware to
  391. # my channel.  When I dispose of the channel, the Sound Manager will
  392. # purge this resource and disconnect me from the hardware.  When adding
  393. # the 'snth' resource, the Sound Manager will allocate a pointer into
  394. # the application’s heap instead of the system’s.  This is a modifier
  395. # stub used by the 'snth'.  This could cause some problems with memory
  396. # management.  I create and dispose of all my channels as soon as
  397. # possible, and this doesn’t cause me problems.
  398. #
  399. # I keep track of which document is playing a sound, along with a
  400. # global of when the application is playing sound.  I needed to keep
  401. # track of which document is playing because if the user disposes of
  402. # that document, I will have to stop playing the sound contained in it
  403. # since the user wants to dispose of that data.  I keep track of when
  404. # the application is playing sound in a global.  This is only used by
  405. # the routine that calculates the sleep time for WaitNextEvent.
  406. #
  407. # I came up with a pretty sick music notational system using Rez.
  408. # Refer to the notes in the SoundAppSnds.r file.  If you’ve just
  409. # finished a meal, wait four hours before reading.
  410. #
  411. # The SoundUnit handles all of the Sound Manager code entirely.  This
  412. # eliminates any and all references to the Sound Manager from the
  413. # application.  The SoundUnit will return any error encountered while
  414. # calling the Sound Manager, and does some extra error checking the
  415. # Sound Manager doesn’t do.
  416. #
  417. # The portion of the application that uses the wave table synthesizer
  418. # is more complex than the other two.  I wanted to include an example
  419. # channel modifier for use in the wave table channels.  This would have
  420. # been a transpositional modifier that would take a given freqDurationCmd and
  421. # transpose it by some amount.  This would be nice for the routine that
  422. # plays a scale, by allowing the other three channels to be playing the
  423. # same scale but at a different interval.  Unfortunately, I found that
  424. # the Sound Manager has bugs using a modifier, at least with the wave
  425. # table synths, and could not use them.
  426. #
  427. # I’ve created a few wave table sounds and keep them in a 'snd '
  428. # resource.  This allows me to change the sound of the wave table
  429. # channels and not change any of the code.  Creating wave table data is
  430. # complicated.  The example sounds I’ve included are samples I’ve taken
  431. # from various sources.  I’ve cleaned them up quit a bit.  This was to
  432. # set loop points, try and reduce clicks, correct the sample rates, and
  433. # base frequencies.  This is also a complicated task.  Maybe I should document
  434. # these techniques.
  435. #
  436. # Jim Reekes E.O., Macintosh Developer Technical Support
  437. # Tuesday, January 30, 1990  1:01 PM
  438. #
  439. #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  440.  
  441. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  442. PROGRAM SoundApp;
  443. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  444.  
  445. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  446. USES
  447. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  448.     Types, QuickDraw, Traps, Events, Controls, Windows, TextEdit, Dialogs,
  449.     Fonts, Lists, Menus, Resources, Scrap, ToolUtils, Files, SegLoad,
  450.     Packages,
  451.     DiskInit, LowMem, Script, Errors, CursorCtl, Sound, SoundInput,
  452.     SoundUnit, Devices, StandardFile;
  453.  
  454. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  455. CONST
  456. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  457.     kSysEnvironsVersion =    1;        {Version of the SysEnvRec I understand.}
  458.     kSystem602 =            $0602;    {SysEnvRec value for System version 6.0.2}
  459.     kPollingSleepTime =    60;        {MultiFinder’s sleep while sound is playing}
  460.  
  461.     kNumberOfMasters =     3;            {number of master pointer blocks we expect}
  462.     kSizeOfReserve =        32 * 1024; {size of reserve memory for grow zone proc}
  463.     kMinSpace =                32 * 1024; {minimum available memory I allow in heap}
  464.     kMemForSndDoc =        20 * 1024; {minimal amount of memory needed by document}
  465.  
  466.     rAppSignature =         'SAPP';    {applicaiton’s OS signature}
  467.     rSndAppDocType =        'sDoc';    {document's file type}
  468.     
  469.     kScrollBarAdjust =    15;        {the width of the scrollbar in the list}
  470.     kListFrameInset =        -1;        {inset rectangle adjustment for list frame}
  471.     kMsgInset  =            8;            {inset rectangle adjustment for messages}
  472.  
  473.     kSFTopLeft =            $00280032; {40,50 is topLeft for Std File dialog}
  474.  
  475.     kCntlActivate =        0;            {enabled control’s hilite state}
  476.     kCntlDeactivate =        255;        {disabled control’s hilite state}
  477.     kSelect =                1;            {select the control}
  478.     kDeselect =                0;            {deselect the control}
  479.     kCntlOn =                1;            {control’s value when truned on}
  480.     kCntlOff =                0;            {control’s value when truned off}
  481.  
  482.     kButtonFrameSize =    3;            {button’s frame pen size}
  483.     kButtonFrameInset =    -4;        {inset rectangle adjustment around button}
  484.     kButtonSizeH =            17;        {standard height of buttons}
  485.     kDafaultButSizeH =     kButtonSizeH - kButtonFrameInset - kButtonFrameInset;
  486.  
  487.  
  488. {refer to the SoundApp.r file for the explaination about these numbers}
  489.     kNumOfButtons =        6;            {the number of buttons in the window}
  490.     kSndStdSpacing =        8;            {adjusts relative spacing of the entire window}
  491.     kSndButtonSizeW =        100;        {SizeW of buttons in document window}
  492.     kSndButtonSizeH =        24;        {heigth of buttons in document window}
  493.     kSoundWindowSizeH =    ((kNumOfButtons * kSndStdSpacing)
  494.                                  + (kNumOfButtons * kSndButtonSizeH) + kSndStdSpacing);
  495.     kSoundWindowSizeW =    296;
  496.  
  497.     kIconTop =                10;        {standard position for icons in alerts}
  498.     kIconLeft =                20;
  499.     kIconBot =                42;
  500.     kIconRight =            52;
  501.     kIconPictGap =            8;            {pict spacing in about window}
  502.  
  503.     kFSAsynch =                TRUE;        {asynchronous File Manager call}
  504.  
  505.     kEnterKey =                CHR($03); {the keys I’m looking for}
  506.     kReturnKey =            CHR($0D);
  507.     kEscape =                CHR($1B);
  508.     kUpArrow =                CHR($1E);
  509.     kDownArrow =            CHR($1F);
  510.     kPeriod =                CHR($2E);
  511.     kBackspace =            CHR($08);
  512.     kDelete =                CHR($7F);
  513.  
  514. {This bit set in the ioFlAttrib field if the file’s resource fork is open.}
  515.     kResForkOpenBit =        2;
  516.  
  517. {For the delay time when flashing the menubar and highlighting a button.}
  518.     kDelayTime =            8;            {8/60ths of a second}
  519.  
  520. {The lowest volume I consider before suggesting the user increase it.}
  521.     kMinVolumeDesired =    4;
  522.  
  523. {The minimal number of ticks the ShowWindow needs to be visible.}
  524.     kShowTimeDelay    =        20;
  525.  
  526. {BUG NOTE: Using a timbre value of 255 on the Mac Plus/SE will cause a crash.}
  527.     kSquareWave =            240;         {square wave for squareWaveSynth}
  528.     kSineWave =                0;            {sine wave for the squareWaveSynth}
  529.     kPreferredTimbre =    190;        {my preferred timbre for the squareWaveSynth}
  530.   
  531. {Application snd resources must be higher than this number.}
  532.     kSystemSndRange =     8191;        {This is the highest snd id reserved by Apple.}
  533.  
  534. {The following constants are the resource IDs.}
  535.     rMenuBar =                1000;        {application’s menu bar}
  536.  
  537.     rExitAlert =            1000;        {emergency exit user alert}
  538.     rUserAlert =            1001;        {error message user alert}
  539.     rSoundVolAlert =        1002;        {sound is set low alert}
  540.     rSaveAlert =             1003;        {save changes? dialog}
  541.  
  542.     rGetNameDLOG =         1000;        {get a name for the sound dialog}
  543.     rNameItem =                3;            {edit text item in rGetNameDLOG}
  544.     rUserItem =             5;            {user item to help draw default outline}
  545.     
  546.     rSFPGetFileDLOG =        -4000;    {dialog template for SFPGetFile}
  547.     rSndOnlyCheckBox =    11;        {dialog item number in SFPGetFile}
  548.  
  549. {These are the window IDs used in the applications.  Each one must be unique,
  550.  since they are used to identify which window the event took place in.}
  551.     rAboutWindow =            1000;        {about window}
  552.     rStatusWindow =        1001;        {sound status window}
  553.     rSoundWindow =            1002;        {sound document window}
  554.  
  555.     rListRectID =            1000;        {resource containing size of list rectangle}
  556.  
  557.     rCancelCntl =            1000;        {stop button ID for the status window}
  558.     rPlaySndCntl =            1001;        {sound button IDs for the sound document window}
  559.     rHyperPlayCntl =        1002;
  560.     rPlayScaleCntl =        1003;
  561.     rMelodyCntl =            1004;
  562.     rStopCntl =                1005;
  563.     rRecordCntl =             1006;
  564.     rAboutOkCntl =            1007;
  565.     rUntitled =                1000;        {string ID for untitled resources}
  566.     rAboutText =            1001;        {string ID for text appearing in about window}
  567.     rPutFileMsg =             1002;        {string for text appearing in SFPutFile dialog}
  568.     rMoofIcon =                1000;        {resource ID for ICON of application}
  569.     rAppPict =                1000;        {resource ID of picture shown in about window}
  570.     rSndCursor =            1000;        {cursor resource for our documents}
  571.  
  572. {The following are the snd resource IDs contained in the application,
  573.  which start at ID 9000.  IDs 0 - 8191 are reserved for Apple.}
  574.     rMoofSound =            9000;        {snd for the about window}
  575.     rScaleSnd =                9001;        {snd containing a scale}
  576.     rMelodyPart1 =            9002;        {snd containing a melody}
  577.     rMelodyPart2 =            9003;        {snd containing the harmony}
  578.     rMelodyPart3 =            9004;        {snd containing the harmony}
  579.     rMelodyPart4 =            9005;        {snd containing the harmony}
  580.     rWaveHarmony =         9006;        {snd containing waveTable for harmony}
  581.     rWaveMelody =             9007;        {snd containing waveTable for melody}
  582.     rCounterPt1    =            9008;        {snd containing soprano part of counter point}
  583.     rCounterPt2 =            9009;        {snd containing alto part of counter point}
  584.     rCounterPt3 =            9010;        {snd containing tenor part of counter point}
  585.     rCounterPt4 =            9011;        {snd containing bass part of counter point}
  586.     rSopranoVox =             9012;        {snd containing waveTable of soprano voice}
  587.     rAltoVox =                9013;        {snd containing waveTable of alto voice}
  588.     rTenorVox =                9014;        {snd containing waveTable of tenor voice}
  589.     rBassVox =                9015;        {snd containing waveTable of bass voice}
  590.  
  591. {The following are resource IDs for messages.}
  592.     sErrStrings =            1000;        {error string STR# ID}
  593.     sStandardErr =            1;            {An error has occurred.}
  594.     sMemErr =                2;            {A Memory Manager error has occurred.}
  595.     sResErr =                3;            {A Resource Manager error has occurred.}
  596.     sCurInUseErr =            4;            {That file is currently in use.}
  597.     sWavesBroken =            5;            {The wave table synthesizer is not available.}
  598.     sWrongVersion =        6;            {This system does not support the Sound Manager...}
  599.     sLowMemory =            7;            {Memory is too low to continue...}
  600.     sNoMenus =                8;            {Could not find application’s menu resources.}
  601.     sInitSoundErr =        9;            {Could not initialize the Sound unit.}
  602.     sSoundErr =                10;        {The Sound Manager has encountered an error.}
  603.     sNewDocErr =            11;        {Could not create a new document.}
  604.     sInitStatusErr =        12;        {Error initializing the status window.}
  605.     sEditErr =                13;        {Could not complete the edit command.}
  606.     sDocErr =                14;        {There is a problem with this document.}
  607.  
  608.     sMsgStrings =            1001;        {message string STR# ID}
  609.     sPlayingMsg =            1;            {playing a sound}
  610.     sHyperMsg =                2;            {playing a sound the Hyper way}
  611.     sScaleMsg =                3;            {playing scale}
  612.     sMelodyMsg =            4;            {playing melody}
  613.     sTimbresMsg =            5;            {playing various timbres}
  614.     sCounterPtMsg =        6;            {playing 4 part counter point}
  615.  
  616.     sSMErrStrings =        1002;        {strings describing Sound Manager errors}
  617.  
  618. {The following constants are used to identify menus and items. The menu IDs
  619.  have an “m” prefix and the item numbers within each menu have an “i” prefix.}
  620.     mApple =                    128;        {Apple menu and items}
  621.     iAbout =                    1;
  622.  
  623.     mFile =                    129;        {File menu and items}
  624.     iNew =                    1;
  625.     iOpen =                    2;
  626.     iClose =                    4;
  627.     iQuit =                    12;
  628.  
  629.     mEdit =                    130;        {Edit menu and items}
  630.     iUndo =                    1;
  631.     iCut =                    3;
  632.     iCopy =                    4;
  633.     iPaste =                    5;
  634.     iClear =                    6;
  635.  
  636.     mDemos =                1000;            {Demos menu and items}
  637.     iSquareScale =            1;
  638.     iSquareMelody =        2;
  639.     iSquareTimbre =        3;
  640.     iWaveScale =            5;
  641.     iWaveMelody =            6;
  642.     iWaveSATB =                7;
  643.  
  644. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  645. TYPE
  646. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  647.  
  648.     BooleanPtr =        ^BOOLEAN;            {Ptr to a BOOLEAN, for type coercion}
  649. {    IntegerPtr =        ^INTEGER;            Ptr to a INTEGER, for type coercion}
  650. {    LongIntPtr =        ^LongInt;            Ptr to a LONGINT, for type coercion}
  651. {    RectPtr =            ^Rect;                Ptr to a Rect resource for coercion}
  652.     RectHandle =        ^RectPtr;            {Handle to a Rect resource for coercion}
  653.  
  654. {This is a document layout that contains the window record and references
  655.  to the associating data.  The window record MUST be the first field.  This
  656.  is because I use the window pointer returned by the Toolbox to be a
  657.  pointer to my document.  To confirm that the window pointer is a document
  658.  pointer, I store an application reference in the window record’s refCon.
  659.  Then, I use a routine to test for the presence of this reference to insure
  660.  I’m looking at one of my document’s windows.}
  661.  
  662.     SndDocument =     RECORD
  663.         window:        WindowRecord;            {must be first field}
  664.         resFile:        INTEGER;                    {document’s resource file}
  665.         vRefNum:        INTEGER;                    {real volume that contains the doc}
  666.         dirID:        LONGINT;                    {the dirID that contains the doc}
  667.         list:            ListHandle;                {document’s list of sounds}
  668.         sndInUse:    BOOLEAN;                    {document is using a 'snd ' resource}
  669.     END;
  670.     SndDocPeek =    ^SndDocument;            {to peek at the document record}
  671.  
  672. {This is the status window layout.  The concept here is similar to the
  673.  document type mentioned above.  The message is a string handle used to
  674.  store the current message.}
  675.  
  676.     StatusWindow = RECORD
  677.         window:        WindowRecord;
  678.         message:        StringHandle;            {current text of status message}
  679.         showTime:    LONGINT;                    {time window was shown}
  680.     END;
  681.     StatWindowPeek = ^StatusWindow;
  682.  
  683. {This is the about window layout.  The concept here is similar to the
  684.  document type mentioned above.  The comment is a string handle used to
  685.  store the current message.}
  686.  
  687.     AboutWindow =    RECORD
  688.         window:        WindowRecord;
  689.         comment:        Handle;                    {handle to string of about comments}
  690.         appIcon:        Handle;                    {handle to icon or color icon}
  691.         iconIsColor:Boolean;                    {TRUE if above icon is color}
  692.         appPict:        Handle;                    {handle to picture of app’s name}
  693.     END;
  694.     AboutWPeek =    ^AboutWindow;
  695.  
  696. {This is the template to the WIND resource.  I used it to load in the WIND
  697.  resource and then adjust the boundsRect.  I also look at the procID to
  698.  determine if it has a title bar or drag region.}
  699.  
  700.     WindowTemplate =    RECORD                {template to a WIND resource}
  701.         boundsRect:    Rect;
  702.         procID:        INTEGER;
  703.         visible:     BOOLEAN;
  704.         filler1:     BOOLEAN;
  705.         goAwayFlag: BOOLEAN;
  706.         filler2:     BOOLEAN;
  707.         refCon:        LongInt;
  708.         title:        Str255;
  709.     END;
  710.     WindowTPtr =    ^WindowTemplate;
  711.     WindowTHndl =    ^WindowTPtr;
  712.  
  713. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  714. VAR {The “g” prefix is used to emphasize that a variable is global.}
  715. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  716.  
  717. {gMac is used to hold the result of a SysEnvirons call. This makes it
  718.  convenient for any routine to check the environment. It is considered
  719.  global information, anyway.}
  720.  
  721.     gMac:                        SysEnvRec;        {set up by Initialize}
  722.  
  723. {gReserveMemory is a handle of reserve memory used by my grow zone
  724.  procedure.  If memory is attempted to be allocated and fails, my grow
  725.  zone will release this reserved block of memory.}
  726.  
  727.     gReserveMemory:        Handle;
  728.  
  729. {gStatusWindow is the window that tells the user what’s happening.  I show
  730.  it while a sound is playing, and remove it as soon as the sound is
  731.  complete.  It is initialized once during startup, and from then on two
  732.  routines are used to show and hide it.  This demonstrates orchestrating
  733.  multimedia, that sound/graphic idea, at least in a crude sort of way.}
  734.  
  735.     gStatusWindow:            StatWindowPeek;
  736.  
  737. {gAppResRef is the application’s resource file reference.  I need to save
  738.  this since I can open other resource files.  The current resource file is
  739.  always gAppResRef unless I momentarily set it to another file to read its
  740.  resources, and then immediately restore it back.}
  741.  
  742.     gAppResRef:             INTEGER;            {set up by Initialize}
  743.  
  744. {gInBackground is maintained by our osEvent handling routines. Any part of
  745.  the program can check it to find out if it is currently in the background.}
  746.  
  747.     gInBackground:         BOOLEAN;            {maintained by Initialize and DoEvent}
  748.  
  749. {gSndFilesOnly is used to determine if the check box in the custom
  750.  SFPGetFile dialog has been chosen.  If this is true, then I want to only
  751.  show files that contain 'snd ' resources.}
  752.  
  753.     gSndFilesOnly:            BOOLEAN;            {maintained by SFGetHook}
  754.  
  755.  
  756. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  757. { IMPLEMENTATION }
  758. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  759. {$S Main}
  760. FUNCTION MyGrowZone(cbNeeded: Size): LONGINT;
  761.  
  762. {This is a very basic grow zone procedure.  My application keeps a reserve
  763.  handle of memory in case the Memory Manager gets a request for some memory
  764.  that is not available in my heap.  If memory were to get tight (<32k),
  765.  the Toolbox will crash the system, especially Quickdraw.  Before releasing
  766.  the reserve handle I make sure it isn’t the GZSaveHnd.  This handle cannot
  767.  be touched by the grow zone procedure.
  768.  
  769.  WARNING:  The grow zone procedure will be called and A5 may not be valid.
  770.  Read Tech Note #136 and 208}
  771.  
  772. VAR
  773.     theA5:         LONGINT;
  774.  
  775. BEGIN
  776.     theA5:= SetCurrentA5;
  777.     IF (gReserveMemory^ <> NIL) & (gReserveMemory <> GZSaveHnd) THEN BEGIN
  778.         EmptyHandle(gReserveMemory);
  779.         MyGrowZone:= kSizeOfReserve;                {released this much memory}
  780.     END ELSE
  781.         MyGrowZone:= 0;                                {this may release more memory}
  782.     theA5:= SetA5(theA5);
  783. END; {MyGrowZone}
  784.  
  785. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  786. {$S Main}
  787. FUNCTION LowOnReserve: BOOLEAN;
  788.  
  789. VAR
  790.     total, contig:            LONGINT;
  791.  
  792. {Before my application attempts to use more memory, I call this routine
  793.  to check if I’m already using my reserve memory.  If so, then I better
  794.  prepare to die or get my reserve back.}
  795.  
  796. BEGIN
  797.     LowOnReserve:= gReserveMemory^ = NIL;        {empty handle is low reserve}
  798. END; {LowOnReserve}
  799.  
  800. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  801. {$S Main}
  802. PROCEDURE RecoverReserve;
  803.  
  804. {This is called from the event loop if LowOnReserve returns that I’m out of
  805.  the reserve memory.  This will recover the reserve memory block.  If this
  806.  fails, it will be called the next time through the event loop.}
  807.  
  808. BEGIN
  809.     ReallocateHandle(gReserveMemory, kSizeOfReserve);
  810. END; {RecoverReserve}
  811.  
  812. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  813. {$S Initialization}
  814. FUNCTION AllocateReserve: BOOLEAN;
  815.  
  816. {This is called at startup time to allocate the reserve memory block used
  817.  in the grow zone procedure.  If I’m unable to obtain this reserve, then
  818.  return FALSE to signal a failure.}
  819.  
  820. BEGIN
  821.     gReserveMemory:= NewHandle(kSizeOfReserve);
  822.     AllocateReserve:= gReserveMemory <> NIL;
  823. END; {AllocateReserve}
  824.  
  825. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  826. {$S Main}
  827. FUNCTION FailLowMemory(memRequest: LONGINT): BOOLEAN;
  828.  
  829. {Call PurgeSpace and see if the requested amount of memory exists in the
  830.  heap including a minimal amount of heap space.  Also, if my grow zone’s
  831.  reserve has been release, that’s considered a failure.  I don’t perform
  832.  any purging here.  The Memory Manager will do this if it needs the space.
  833.  This routine can be called with a memRequest = 0.  This checks if the heap
  834.  space is getting critical.}
  835.  
  836. VAR
  837.     total, contig:        LONGINT;
  838.  
  839. BEGIN
  840.     PurgeSpace(total, contig);
  841.     FailLowMemory:= (total < (memRequest + kMinSpace)) | LowOnReserve;
  842. END; {FailLowMemory}
  843.  
  844. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  845. {$S Main}
  846. FUNCTION HasSelection(sndDoc: SndDocPeek): BOOLEAN;
  847.  
  848. {This a simple test to see if the user has a currently selected list item.}
  849.  
  850. VAR
  851.     aCell:            Cell;
  852.  
  853. BEGIN
  854.     SetPt(aCell, 0, 0);
  855.     HasSelection:= LGetSelect(TRUE, aCell, sndDoc^.list)
  856. END; {HasSelection}
  857.  
  858. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  859. {$S Main}
  860. FUNCTION GetSelection(sndDoc: SndDocPeek; VAR sndHandle: Handle): OSErr;
  861.  
  862. {Given a document, this will find the currently selected cell in the list.
  863.  Then using this cell as an index number, I call Get1IndResource.  This
  864.  will return the proper handle.  Since the List Manager is zero based and
  865.  the Resource Manager isn’t, I have to add one to the index.
  866.  
  867.  BUG NOTE: GetIndResource will return a bogus handle if the index is not
  868.  positive.}
  869.  
  870. VAR
  871.     aCell:             Cell;
  872.  
  873. BEGIN
  874.     GetSelection:= noErr;
  875.     SetPt(aCell, 0, 0);
  876.     IF LGetSelect(TRUE, aCell, sndDoc^.list) THEN BEGIN
  877.         IF aCell.v > -1 THEN BEGIN                {GetIndResource doesn’t like < 0}
  878.             UseResFile(sndDoc^.resFile);        {only get our resources}
  879.             sndHandle:= Get1IndResource('snd ', aCell.v + 1);
  880.             IF sndHandle = NIL THEN
  881.                 GetSelection:= ResError;        {return any error}
  882.             UseResFile(gAppResRef);                {restore our resource file}
  883.         END;
  884.     END;
  885. END; {GetSelection}
  886.  
  887. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  888. {$S Main}
  889. PROCEDURE SelectNextCell(list: ListHandle; next: BOOLEAN);
  890.  
  891. {This is used to allow the user to select items from the list by using
  892.  the arrow keys.  Given a list and the direction, this will select the
  893.  next cell.  It will do nothing if we’re at the first cell and the user
  894.  wants to do even higher (sped), or if we’re at the last cell and the
  895.  user wants to go even lower (rent a clue bud).}
  896.  
  897. VAR
  898.     aCell:            Cell;
  899.     lastItem:        INTEGER;
  900.  
  901.     PROCEDURE DoNextSelection;
  902.     BEGIN
  903.         LSetSelect(FALSE, aCell, list);
  904.         IF next THEN
  905.             aCell.v:= aCell.v + 1
  906.         ELSE
  907.             aCell.v:= aCell.v - 1;
  908.         SetPt(aCell, aCell.h, aCell.v);
  909.         LSetSelect(TRUE, aCell, list);
  910.         LAutoScroll(list);
  911.     END;
  912.  
  913. BEGIN
  914.     lastItem:= list^^.dataBounds.bottom - 1;        {bounds is 1 greater}
  915.     SetPt(aCell, 0, 0);
  916.     IF LGetSelect(TRUE, aCell, list) THEN BEGIN
  917.         IF (next & (aCell.v < lastItem)) | ((NOT next) & (aCell.v > 0)) THEN
  918.             DoNextSelection;
  919.     END ELSE                                                    {if no cells selected...}
  920.         LSetSelect(TRUE, aCell, list);                {select the first cell}
  921. END; {SelectNextCell}
  922.  
  923. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  924. {$S Main}
  925. PROCEDURE SelectSndCell(sndDoc: SndDocPeek; resID: INTEGER);
  926.  
  927. {VERSION 1.1:  This routine will go through the list of snd resources  
  928.  looking for the one that has the resource ID matching the parameter passed   
  929.  in.  First thing to do is deselect the current selection.  Then once the  
  930.  matching resource is found, select that one and scroll it into view.  One  
  931.  assumption made is that the resources and the list are both index in the   
  932.  same order.  This is true for the sndDoc, since I built the list this way.}
  933.  
  934. VAR
  935.     name:                    Str255;
  936.     aCell:                Cell;
  937.     sndHndle:            Handle;
  938.     rType:                ResType;
  939.     theErr:                OSErr;
  940.     testID:                INTEGER;
  941.     index:                INTEGER;
  942.     numSnd:                INTEGER;
  943.     ignore:                BOOLEAN;
  944.  
  945. BEGIN
  946.     index:= 0;
  947.     SetPt(aCell, 0, 0);
  948.     IF LGetSelect(TRUE, aCell, sndDoc^.list) THEN
  949.         LSetSelect(FALSE, aCell, sndDoc^.list); {deselect any cell}
  950.     UseResFile(sndDoc^.resFile);                {count only its resources}
  951.     numSnd:= Count1Resources('snd ');        {number of sounds available}
  952.     REPEAT
  953.         index:= index + 1;
  954.         SetResLoad(FALSE);                        {don’t load any resources}
  955.         sndHndle:= Get1IndResource('snd ', index); {only get snd from file}
  956.         SetResLoad(TRUE);                            {back to normal resource operations}
  957.         GetResInfo(sndHndle, testID, rType, name);
  958.     UNTIL ((resID = testID) | (index > numSnd));
  959.     UseResFile(gAppResRef);                        {restore our resource file}
  960.     SetPt(aCell, 0, index - 1);
  961.     LSetSelect(TRUE, aCell, sndDoc^.list);
  962.     LAutoScroll(sndDoc^.list);
  963. END; {SelectSndCell}
  964.  
  965. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  966. {$S Open}
  967. FUNCTION InitSndList(docPtr: SndDocPeek): OSErr;
  968.  
  969. {This is used to create the list showing all the snd resources in the file.
  970.  If anything goes wrong while attempting to get any of the resources, then I
  971.  return false.  I don’t want the document to continue if something goes
  972.  wrong while reading a resource.  If I do get the resource, then I add its
  973.  name to the list in a new row.  I’m assured that at least one resource is
  974.  in the file before this routine is called.  Untitled resources will get one.}
  975.  
  976. VAR
  977.     index, resID,
  978.     newRow, numSnd:    INTEGER;
  979.     resHandle:            Handle;
  980.     strHandle:            StringHandle;
  981.     itsType:                ResType;
  982.     resName, untitled:Str255;
  983.     aCell:                Cell;
  984.  
  985. BEGIN
  986.     InitSndList:= noErr;
  987.     strHandle:= StringHandle(Get1Resource('STR ', rUntitled));
  988.     IF strHandle <> NIL THEN
  989.         untitled:= strHandle^^                    {save no name title}
  990.     ELSE
  991.         untitled:= '';                                {at least an empty string}
  992.     UseResFile(docPtr^.resFile);
  993.     numSnd:= Count1Resources('snd ');
  994.     newRow:= LAddRow(numSnd, 0, docPtr^.list);
  995.     FOR index:= 1 TO numSnd DO BEGIN
  996.         SetResLoad(FALSE);                        {don’t load any resources}
  997.         resHandle:= Get1IndResource('snd ', index); {only get snd from file}
  998.         SetResLoad(TRUE);                            {back to normal resource operations}
  999.         IF resHandle <> NIL THEN BEGIN        {only if I got the snd}
  1000.             GetResInfo(resHandle, resID, itsType, resName);
  1001.             IF resName = '' THEN                    {if the snd isn’t named...}
  1002.                 resName:= untitled;                {give it a name}
  1003.             SetPt(aCell, 0, index - 1);
  1004.             LSetCell(Ptr(ORD(@resName) + 1), LENGTH(resName), aCell, docPtr^.list);
  1005.         END ELSE BEGIN {resHandle = NIL}
  1006.             InitSndList:= resNotFound;            {problem with resource file}
  1007.             index:= numSnd + 1;                    {get out of the loop}
  1008.         END;
  1009.     END;
  1010.     UseResFile(gAppResRef);                        {restore our resource file}
  1011. END;
  1012.  
  1013. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1014. {$S Open}
  1015. FUNCTION OpenByApp(vRefNum: INTEGER; dirID: LONGINT;
  1016.                         fName: StringPtr; VAR window: WindowPtr): BOOLEAN;
  1017.  
  1018. {In the case that the user has attempted to open a resource file that is
  1019.  already open, this routine will scan my open documents for it.  If I find
  1020.  that the requested file is already open by the application, then return
  1021.  true and its window pointer.  The application can then simply select that
  1022.  window.  If the resource file is open but not by this application, that’s
  1023.  a problem and I don’t use the resource file.  Many problems with using an
  1024.  open resource file.  Now if the Resource Manager would take care of this
  1025.  problem for us...}
  1026.  
  1027. VAR
  1028.     docName:                    Str255;
  1029.     testWindow:                WindowPtr;
  1030.  
  1031. BEGIN
  1032.     OpenByApp:= FALSE;
  1033.     testWindow:= FrontWindow;
  1034.     WHILE testWindow <> NIL DO BEGIN
  1035.         IF GetWRefCon(testWindow) = rSoundWindow THEN
  1036.             IF (vRefNum = SndDocPeek(testWindow)^.vRefNum)
  1037.              & (dirID = SndDocPeek(testWindow)^.dirID) THEN BEGIN
  1038.                  GetWTitle(testWindow, docName);
  1039.                 IF fName^ = docName THEN BEGIN
  1040.                     window:= testWindow;
  1041.                     OpenByApp:= TRUE;
  1042.                     EXIT(OpenByApp);
  1043.                 END;
  1044.             END;
  1045.         testWindow:= WindowPtr(WindowPeek(testWindow)^.nextWindow);
  1046.     END;
  1047. END; {OpenByApp}
  1048.  
  1049. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1050. {$S Main}
  1051. FUNCTION GetGlobalMouse: Point;
  1052.  
  1053. {Get the global coordinates of the mouse.  Get the global coordinates by
  1054.  calling GetMouse and LocalToGlobal.  This assumes the current port is a
  1055.  valid graf port.  When wouldn’t it be?}
  1056.  
  1057. VAR
  1058.     globalPt:        Point;
  1059.  
  1060. BEGIN
  1061.     GetMouse(globalPt);
  1062.     LocalToGlobal(globalPt);
  1063.     GetGlobalMouse:= globalPt;
  1064. END; {GetGlobalMouse}
  1065.  
  1066. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1067. {$S Open}
  1068. FUNCTION GetWTitleHeight(variant: INTEGER): INTEGER;
  1069.  
  1070. {Try and determine the window’s title bar height.  This isn’t easy,
  1071.  especially if the window is invisible.  We all know how the standard Apple
  1072.  System WDEF works, at least at this date we do.  I assume that method as
  1073.  shown below.  I could do what MacApp does with the Function CallWDefProc.
  1074.  If the user has installed a replacement for the System WDEF, then it’s
  1075.  their problem to deal with.  My method will work as long as Apple doesn’t
  1076.  change how the WDEF in System 6.0 calculates the title bar height.  This
  1077.  will allow my application to work on international Macs that have a larger
  1078.  system font than Chicago.  I check the window’s variant code for one that
  1079.  includes a title bar.  I will have to change this routine to adjust for
  1080.  the modal-moveable window type, which hasn’t been defined yet.
  1081.  
  1082.  In this routine, I violate my rule about not using the GetPort, SetPort,
  1083.  SetPort sequence mentioned at the start of the file. Mostly, I do this
  1084.  because it’s not all that apparent that a routine called GetWTitleHeight
  1085.  will change the port, so I make sure that it doesn’t.}
  1086.  
  1087. VAR
  1088.     info:                FontInfo;
  1089.     curGraf,
  1090.     wMgrPort:        GrafPtr;
  1091.     wTitleHeight:    INTEGER;
  1092.  
  1093. BEGIN
  1094.     IF variant IN [documentProc, noGrowDocProc,
  1095.                         zoomDocProc, zoomNoGrow, rDocProc] THEN BEGIN
  1096.         GetPort(curGraf);
  1097.         GetWMgrPort(wMgrPort);                        {I need to know the font...}
  1098.         SetPort(wMgrPort);                            {info in the System’s port}
  1099.         GetFontInfo(info);
  1100.         SetPort(curGraf);                                {restore current port}
  1101.         WITH info DO
  1102.             wTitleHeight:= ascent + descent + leading + 2;
  1103.         IF wTitleHeight < 19 THEN
  1104.             wTitleHeight:= 19;                        {the title is always at least 19}
  1105.         GetWTitleHeight:= wTitleHeight;
  1106.     END ELSE
  1107.         GetWTitleHeight:= 0;                            {other window types have no title}
  1108. END; {GetWTitleHeight}
  1109.  
  1110. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1111. {$S Main}
  1112. FUNCTION GetGlobalTopLeft(window: WindowPtr): Point;
  1113.  
  1114. {Given a window, this will return the top left point of the window’s
  1115.  port in global coordinates.  Something this doesn’t include, is the
  1116.  window’s drag region (or title bar).  This returns the top left point
  1117.  of the window’s content area only.
  1118.  
  1119.  In this routine, I violate my rule about not using the GetPort, SetPort,
  1120.  SetPort sequence mentioned at the start of the file. Mostly, I do this
  1121.  because it’s not all that apparent that a routine called GetGlobalTopLeft
  1122.  will change the port, so I make sure that it doesn’t.}
  1123.  
  1124. VAR
  1125.     theGraf:            GrafPtr;
  1126.     globalPt:        Point;
  1127.  
  1128. BEGIN
  1129.     GetPort(theGraf);
  1130.     SetPort(window);
  1131.     globalPt:= window^.portRect.topLeft;
  1132.     LocalToGlobal(globalPt);
  1133.     SetPort(theGraf);
  1134.     GetGlobalTopLeft:= globalPt;
  1135. END; {GetGlobalTopLeft}
  1136.  
  1137. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1138. {$S Main}
  1139. PROCEDURE MyParamText(text: StringHandle; cite0, cite1: StringPtr);
  1140.  
  1141. {I’m using string pointers (not necessarily Memory Manager pointers)
  1142.  to the replacement strings.  Using Str255 would copy the string to the
  1143.  stack, and then to the text handle.  This way it is only copied once.}
  1144.  
  1145. VAR
  1146.     offSet:            LONGINT;
  1147.     param0,
  1148.     param1:            PACKED ARRAY [1..2] OF CHAR;
  1149.     newLength:        LONGINT;
  1150.  
  1151. BEGIN
  1152.     param0:= '^0';
  1153.     param1:= '^1';
  1154.     IF cite0^ <> '' THEN
  1155.         offSet:= Munger(Handle(text), 1, @param0, SIZEOF(param0),
  1156.                              Ptr(ORD4(cite0) + 1), LENGTH(cite0^));
  1157.     IF cite1^ <> '' THEN
  1158.         offSet:= Munger(Handle(text), 1, @param1, SIZEOF(param1),
  1159.                              Ptr(ORD4(cite1) + 1), LENGTH(cite1^));
  1160.     newLength:= GetHandleSize(Handle(text)) - 1;
  1161.     IF newLength > 255 THEN
  1162.         newLength:= 255;
  1163.     text^^[0]:= CHAR(newLength);                        {string’s new length byte}
  1164. END;
  1165.  
  1166. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1167. {$S Open}
  1168. PROCEDURE CenterWindowRect(variant: INTEGER; VAR theRect: Rect);
  1169.  
  1170. {Given a window’s portRect, this routine will center the rectangle on the
  1171.  main device within the desktop region.  This excludes the menu bar area.
  1172.  It follows the Apple Human Interface Guidelines for where to place a
  1173.  window centered on the screen. That is, 1/3th of the desktop shows above
  1174.  the window and 2/3ths below it.  It returns a new rectangle set to the
  1175.  centered position.  The top and left of this rectangle can be used with
  1176.  MoveWindow.  This routine only considers the main device.  CenterWindowRect
  1177.  could take a GDevice as a parameter to center the VAR rect with.  This
  1178.  would also allow Alerts or dialogs that are closely associated with a
  1179.  document window to be centered relative to the monitor that contains that
  1180.  document.  This is also one of the Human Interface Guidelines.  MacApp 2.0
  1181.  has a utility CalcScreenRect that you can borrow from to include this.
  1182.  
  1183.  WARNING: This routine may move or purge memory.}
  1184.  
  1185. VAR
  1186.     rectSize:         Point;
  1187.     wTitleHeight:    INTEGER;
  1188.  
  1189. BEGIN
  1190.     wTitleHeight:= GetWTitleHeight(variant);                {get title height}
  1191.     WITH theRect DO BEGIN                                        {get size of rect}
  1192.         SetPt(rectSize, right, bottom + wTitleHeight);    {include it in size}
  1193.         SubPt(topLeft, rectSize);
  1194.     END;
  1195.     WITH qd.screenBits.bounds DO BEGIN                            {1/3th below menubar}
  1196.         theRect.top:= ((bottom - top - GetMBarHeight - rectSize.v) DIV 3)
  1197.                           + GetMBarHeight;
  1198.         theRect.left:= ((right - left) - rectSize.h) DIV 2;    {centered horz}
  1199.     END;
  1200.     WITH theRect DO BEGIN                                        {return adjusted rect}
  1201.         SetPt(botRight, left, top);
  1202.         AddPt(rectSize, botRight);
  1203.         top:= top + wTitleHeight;                    {remove title height from rect}
  1204.     END;
  1205. END; {CenterWindowRect}
  1206.  
  1207. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1208. {$S Open}
  1209. FUNCTION GetCenteredWindow(id: INTEGER; p: Ptr; behind: WindowPtr): WindowPtr;
  1210.  
  1211. {Given a window ID, this routine will center the window’s rectangle before
  1212.  showing it on the main screen.  This follows the Apple Human Interface
  1213.  Guidelines for where to place a centered window on the screen.  If the
  1214.  window is closely associated with another window, considerations should be
  1215.  given to where that other window is located.  If this other window is on a
  1216.  screen other then the main monitor, then it would be best to center the
  1217.  window on that other monitor.
  1218.  
  1219.  VERSION 1.1: There is a problem with GetNewWindow in the old Mac Plus and
  1220.  SE ROMS.  It will call ReleaseResource on the 'WIND' resource within 
  1221.  GetNewWindow.  This make the handle invalid after the call.  Therefore,
  1222.  before calling HPurge, we get the resource once again.}
  1223.  
  1224. VAR
  1225.     newRect:            Rect;
  1226.     windTemplate:    WindowTHndl;
  1227.     window:            WindowPtr;
  1228.  
  1229. BEGIN
  1230.     window:= NIL;                                                {initialize result}
  1231.     windTemplate:= WindowTHndl(Get1Resource('WIND', id));
  1232.     IF windTemplate <> NIL THEN BEGIN
  1233.         HNoPurge(Handle(windTemplate));
  1234.         newRect:= windTemplate^^.boundsRect;
  1235.         CenterWindowRect(windTemplate^^.procID, newRect);
  1236.         windTemplate^^.boundsRect:= newRect;
  1237.         window:= GetNewWindow(id, p, behind);
  1238.         HPurge(Get1Resource('WIND', id));
  1239.     END;
  1240.     GetCenteredWindow:= window;
  1241. END;
  1242.  
  1243. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1244. {$S Open}
  1245. FUNCTION CenteredAlert(alertID: INTEGER): INTEGER;
  1246.  
  1247. {Given an Alert ID, this routine will center the alert’s rectangle before
  1248.  showing it on the main screen.  This follows the Apple Human Interface
  1249.  Guidelines for where to place a centered window on the screen.  If the
  1250.  Alert is closely associated with another window, considerations should be
  1251.  given to what the window is located.  If this other window is on a screen
  1252.  other then the main monitor, then it would be best to center the Alert on
  1253.  that other monitor.}
  1254.  
  1255. VAR
  1256.     alertHandle:    AlertTHndl;
  1257.     alertRect:        Rect;
  1258.     itemHit:            INTEGER;
  1259.  
  1260. BEGIN
  1261.     alertHandle:= AlertTHndl(Get1Resource('ALRT', alertID));
  1262.     IF alertHandle <> NIL THEN BEGIN
  1263.         HNoPurge(Handle(alertHandle));
  1264.         alertRect:= alertHandle^^.boundsRect;
  1265.         CenterWindowRect(dBoxProc, alertRect);
  1266.         alertHandle^^.boundsRect:= alertRect;
  1267.         HPurge(Handle(alertHandle));
  1268.     END;
  1269.     CenteredAlert:= Alert(alertID, NIL);
  1270. END; {CenteredAlert}
  1271.  
  1272. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1273. {$S Open}
  1274. FUNCTION GetCenteredDialog(id: INTEGER; p: Ptr; behind: WindowPtr): DialogPtr;
  1275.  
  1276. {Given a dialog ID, this routine will center the dialog’s rectangle before
  1277.  showing it on the main screen.  This follows the Apple Human Interface
  1278.  Guidelines for where to place a centered window on the screen.  If the
  1279.  dialog is closely associated with another window, considerations should be
  1280.  given to what the window is located.  If this other window is on a screen
  1281.  other then the main monitor, then it would be best to center the dialog on
  1282.  that other monitor.
  1283.   
  1284.  VERSION 1.1: There is a problem with GetNewDialog in the old Mac Plus and
  1285.  SE ROMS.  It will call ReleaseResource on the 'WIND' resource within 
  1286.  GetNewWindow.  This make the handle invalid after the call.  Therefore,
  1287.  before calling HPurge, we get the resource once again.}
  1288.  
  1289. VAR
  1290.     newRect:            Rect;
  1291.     dlogTemplate:    DialogTHndl;
  1292.     dialog:            DialogPtr;
  1293.  
  1294. BEGIN
  1295.     dialog:= NIL;                                                {initialize result}
  1296.     dlogTemplate:= DialogTHndl(Get1Resource('DLOG', id));
  1297.     IF dlogTemplate <> NIL THEN BEGIN
  1298.         newRect:= dlogTemplate^^.boundsRect;
  1299.         CenterWindowRect(dlogTemplate^^.procID, newRect);
  1300.         dlogTemplate^^.boundsRect:= newRect;
  1301.         HNoPurge(Handle(dlogTemplate));
  1302.         dialog:= GetNewDialog(id, p, behind);
  1303.         HPurge(Get1Resource('DLOG', id));
  1304.     END;
  1305.     GetCenteredDialog:= dialog;
  1306. END; {GetCenteredDialog}
  1307.  
  1308. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1309. {$S Main}
  1310. PROCEDURE DoButtonOutline(button: ControlHandle);
  1311.  
  1312. {Given any control handle, this will draw an outline around it.  This is
  1313.  used for the default button of a window.  The extra nice feature here is
  1314.  that I’ll erase the outline for buttons that are inactive.  Seems like
  1315.  there should be a Toolbox call for getting a control’s hilite state.
  1316.  Since there isn’t, I have to look into the control record myself.  This
  1317.  should be called for update and activate events.
  1318.  
  1319.  The method for determining the oval diameters for the roundrect is a
  1320.  little different than that recommended by Inside Mac.  IM I-407 suggests
  1321.  that you use a hardcoded (16,16) for the diameters. However, this only
  1322.  looks good for small roundrects. For larger ones, the outline doesn’t
  1323.  follow the inner roundrect because the CDEF for simply buttons doesn’t
  1324.  use (16,16). Instead, it uses half the height of the button as the
  1325.  diameter. By using this formula, too, our outlines look better.
  1326.  
  1327.  WARNING: This will set the current port to the control’s window.}
  1328.  
  1329. VAR
  1330.     theRect:            Rect;
  1331.     curPen:            PenState;
  1332.     buttonOval:        INTEGER;
  1333.  
  1334. BEGIN
  1335.     IF button <> NIL THEN BEGIN
  1336.         SetPort(button^^.contrlOwner);
  1337.         GetPenState(curPen);
  1338.         PenNormal;
  1339.         theRect:= button^^.contrlRect;
  1340.         InsetRect(theRect, kButtonFrameInset, kButtonFrameInset);
  1341.         buttonOval := (theRect.bottom - theRect.top) DIV 2;
  1342.         IF (button^^.contrlHilite = kCntlActivate) THEN
  1343.             PenPat(qd.black)
  1344.         ELSE
  1345.             PenPat(qd.gray);
  1346.         PenSize(kButtonFrameSize, kButtonFrameSize);
  1347.         FrameRoundRect(theRect, buttonOval, buttonOval);
  1348.         SetPenState(curPen);
  1349.     END;
  1350. END;
  1351.  
  1352. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1353. {$S Main}
  1354. PROCEDURE SelectButton(button: ControlHandle);
  1355.  
  1356. {Given the button control handle, this will cause the button to look as
  1357.  if it has been clicked in.  This is nice to do for the user if they type
  1358.  return or enter to select the default item.}
  1359.  
  1360. VAR
  1361.     finalTicks:        LONGINT;
  1362.  
  1363. BEGIN
  1364.     HiliteControl(button, kSelect);
  1365.     Delay(kDelayTime, finalTicks);
  1366.     HiliteControl(button, kDeselect);
  1367. END; {SelectButton}
  1368.  
  1369. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1370. {$S Main}
  1371. PROCEDURE ActivateSndCntls(sndDoc: SndDocPeek);
  1372.  
  1373. {Given a sound document, this routine goes through the control list looking
  1374.  for a control that needs to be activated, or deactivated what ever the
  1375.  case maybe.  The Play button is the default button, and it also needs its
  1376.  outline drawn.  The Stop button is always active on the front most window.
  1377.  This is true even if there is no selection made in the window’s list.  The
  1378.  Stop button is a global action, regardless of what is happening in the window.
  1379.  
  1380.  VERSION 1.1:  Stop button is only active while a sound is playing.}
  1381.  
  1382. VAR
  1383.     control:            ControlHandle;
  1384.     cntlRefCon:        LONGINT;
  1385.     activate:        BOOLEAN;
  1386.  
  1387. BEGIN
  1388.     activate:= WindowPeek(sndDoc)^.hilited & HasSelection(sndDoc);
  1389.     control:= WindowPeek(sndDoc)^.controlList;
  1390.     WHILE control <> NIL DO BEGIN
  1391.         cntlRefCon:= GetControlReference(control);
  1392.         IF cntlRefCon IN [rPlaySndCntl, rHyperPlayCntl,
  1393.                                 rPlayScaleCntl, rMelodyCntl] THEN BEGIN
  1394.             IF activate THEN
  1395.                 HiliteControl(control, kCntlActivate)
  1396.             ELSE
  1397.                 HiliteControl(control, kCntlDeactivate);
  1398.             IF cntlRefCon = rPlaySndCntl THEN
  1399.                 DoButtonOutline(control);
  1400.         END; {IF cntlRefCon IN}
  1401.         IF cntlRefCon = rStopCntl THEN BEGIN
  1402.             IF WindowPeek(sndDoc)^.hilited & SndChanOpen THEN
  1403.                 HiliteControl(control, kCntlActivate)
  1404.             ELSE
  1405.                 HiliteControl(control, kCntlDeactivate);
  1406.         END; {cntlRefCon = rStopCntl}
  1407.         IF cntlRefCon = rRecordCntl THEN BEGIN
  1408.             IF WindowPeek(sndDoc)^.hilited THEN
  1409.                 HiliteControl(control, kCntlActivate)
  1410.             ELSE
  1411.                 HiliteControl(control, kCntlDeactivate);
  1412.         END; {cntlRefCon = rRecordCntl}
  1413.     control:= control^^.nextControl;
  1414.     END; {WHILE control <> NIL}
  1415. END;
  1416.  
  1417. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1418. {$S Main}
  1419. FUNCTION IsDAWindow(window: WindowPtr): BOOLEAN;
  1420.  
  1421. {Check if a window belongs to a desk accessory.  This will first test for a
  1422.  NIL window.  DAs will have a negitive windowKind.}
  1423.  
  1424. BEGIN
  1425.     IF window = NIL THEN
  1426.         IsDAWindow:= FALSE
  1427.     ELSE    {DA windows have negative windowKinds}
  1428.         IsDAWindow:= WindowPeek(window)^.windowKind < 0;
  1429. END; {IsDAWindow}
  1430.  
  1431. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1432. {$S Main}
  1433. FUNCTION IsDocWindow(window: WindowPtr): BOOLEAN;
  1434.  
  1435. {Check to see if a window is a document window.  This will first test for a
  1436.  NIL window.  Document windows all have a refCon set to rSoundWindow.  This
  1437.  insures there is document type information after the window record.}
  1438.  
  1439. BEGIN
  1440.     IF window = NIL THEN
  1441.         IsDocWindow:= FALSE
  1442.     ELSE
  1443.         IsDocWindow:= GetWRefCon(window) = rSoundWindow;
  1444. END; {IsDocWindow}
  1445.  
  1446. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1447. {$S Main}
  1448. FUNCTION IsModalWindow(window: WindowPtr): BOOLEAN;
  1449.  
  1450. {This test will return TRUE for a window that needs to be treated as a
  1451.  modal window.  To include additional windows, add the window’s refCon
  1452.  value to the set of modal windows.}
  1453.  
  1454. VAR
  1455.     wRef:                LONGINT;
  1456.  
  1457. BEGIN
  1458.     IF window = NIL THEN
  1459.         IsModalWindow:= FALSE
  1460.     ELSE BEGIN
  1461.         wRef:= GetWRefCon(window);
  1462.         IsModalWindow:= wRef IN [rAboutWindow];        {set of modal windows}
  1463.     END;
  1464. END;
  1465.  
  1466. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1467. {$S Main}
  1468. PROCEDURE KillSound;
  1469.  
  1470. {This is used to free up all the sound data and channels in use.  There are
  1471.  two reasons for using this routine.  One is after a sound has completed
  1472.  the other is to terminate a sound that may be in progress.  For this
  1473.  second reason, this routine should always be used before playing a new
  1474.  sound.  When disposing of a sound channel, this sets all document’s
  1475.  sndInUse flags to FALSE.  I can call this routine at any time.  When I
  1476.  find that the memory reserve is used, I will call this to free up any
  1477.  possible sound resources which tend to be large.  Another important point
  1478.  is the human interface issue of hiding the status window.  It’s possible
  1479.  that  playing some short sounds could cause the status window to disappear
  1480.  before the user had much of a chance to see it.  To solve this, there is a
  1481.  delay that insures the status window was visible for a minimal time.
  1482.  
  1483.  VERSION 1.1:  Update the document's controls since know that the sound
  1484.  is no longer playing the buttons in the document window need to be updated.}
  1485.  
  1486. VAR
  1487.     window:            WindowPtr;
  1488.  
  1489. BEGIN
  1490.     IF SndChanOpen THEN BEGIN                                {if a channel is open...}
  1491.         window:= FrontWindow;                                {set all document’s flags}
  1492.         WHILE window <> NIL DO BEGIN
  1493.             IF IsDocWindow(window) THEN                    {if this is my document...}
  1494.                 SndDocPeek(window)^.sndInUse:= FALSE;    {then it is no longer active}
  1495.             window:= WindowPtr(WindowPeek(window)^.nextWindow);
  1496.         END;
  1497.     END;
  1498.     IF SoundCompletion THEN    BEGIN                            {why were we called?}
  1499.         DoSoundComplete;                                        {from completion}
  1500.         REPEAT                                                    {delay for status window}
  1501.         UNTIL ((TickCount - gStatusWindow^.showTime) > kShowTimeDelay);
  1502.     END ELSE
  1503.         FreeAllChans;                                            {or just because}
  1504.     HideWindow(WindowPtr(gStatusWindow));
  1505.     window:= FrontWindow;
  1506.     IF IsDocWindow(window) THEN
  1507.         ActivateSndCntls(SndDocPeek(window));
  1508. END; {KillSound}
  1509.  
  1510. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1511. {$S Main}
  1512. FUNCTION DoCloseWindow(window: WindowPtr): BOOLEAN;
  1513.  
  1514. {Close any of my windows.  At this point, if there was a document
  1515.  associated with a window, you could do any document saving processing if
  1516.  it has been changed since being opened.  DoCloseWindow would return TRUE
  1517.  if the window actually closed.  FALSE would be returned, as an example, if
  1518.  the user chose “Cancel” in the Save As… dialog.  An important detail is if
  1519.  the window to be close is one of my documents that is currently playing a
  1520.  sound.  We cannot dispose of it yet, since the sound resource owned by the
  1521.  document would be in use by the Sound Manager.  So, I test the document’s
  1522.  sndInUse flag and if it is in use I call KillSound.  The About window
  1523.  contains some resource handles.  I only mark them purgeable, but I could
  1524.  use ReleaseResource and then DisposeHandle.}
  1525.  
  1526. BEGIN
  1527.     DoCloseWindow:= TRUE;
  1528.     CASE GetWRefCon(window) OF
  1529.  
  1530.         rSoundWindow: BEGIN
  1531.             IF SndDocPeek(window)^.sndInUse THEN        {contain a snd in use?}
  1532.                 KillSound;                                        {not any more}
  1533.             IF SndDocPeek(window)^.list <> NIL THEN    {dispose of document data}
  1534.                 LDispose(SndDocPeek(window)^.list);
  1535.             IF SndDocPeek(window)^.resFile <> 0 THEN
  1536.                 CloseResFile(SndDocPeek(window)^.resFile);
  1537.             KillControls(window);
  1538.             CloseWindow(WindowPtr(window));
  1539.             DisposePtr(Ptr(window));                        {dispose of our doc storage}
  1540.         END;
  1541.  
  1542.         rStatusWindow: BEGIN
  1543.             KillControls(WindowPtr(gStatusWindow));
  1544.             DisposeHandle(Handle(gStatusWindow^.message));
  1545.             CloseWindow(WindowPtr(gStatusWindow));
  1546.             DisposePtr(Ptr(gStatusWindow));
  1547.         END;
  1548.  
  1549.         rAboutWindow: BEGIN
  1550.             IF AboutWPeek(window)^.comment <> NIL THEN        {never dispose...}
  1551.                 HPurge(AboutWPeek(window)^.comment);            {a Resource handle}
  1552.             IF AboutWPeek(window)^.appPict <> NIL THEN
  1553.                 HPurge(AboutWPeek(window)^.appPict);
  1554.             IF AboutWPeek(window)^.appIcon <> NIL THEN
  1555.                 IF AboutWPeek(window)^.iconIsColor THEN
  1556.                     DisposeCIcon(CIconHandle(AboutWPeek(window)^.appIcon))
  1557.                 ELSE
  1558.                     HPurge(AboutWPeek(window)^.appIcon);         {disposCIcon}
  1559.             KillControls(window);
  1560.             CloseWindow(WindowPtr(window));
  1561.             DisposePtr(Ptr(window));
  1562.         END;
  1563.  
  1564.         OTHERWISE
  1565.             IF IsDAWindow(window) THEN                        {we can close DAs too}
  1566.                 CloseDeskAcc(WindowPeek(window)^.windowKind);
  1567.  
  1568.     END; {CASE GetWRefCon(window)}
  1569. END; {DoCloseWindow}
  1570.  
  1571. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1572. {$S Main}
  1573. PROCEDURE AlertUser(error: INTEGER; messageID: INTEGER);
  1574.  
  1575. {Display an alert to inform the user of an error.  MessageID acts as an
  1576.  index into a STR# resource of error messages.  This will attempt to
  1577.  replace the error number with a string if the sound is a Sound Manager
  1578.  error.
  1579.  
  1580.  BUG NOTE: GetIndString will return a bogus string if the index is not
  1581.  positive.}
  1582.  
  1583. VAR
  1584.     msg1, msg2:        Str255;
  1585.     theItem:            INTEGER;
  1586.  
  1587. BEGIN
  1588.     UseResFile(gAppResRef);                                {restore our resource file}
  1589.     IF messageID > 0 THEN
  1590.         GetIndString(msg1, sErrStrings, messageID)
  1591.     ELSE
  1592.         msg1:= '';                                            {in case there is no message}
  1593.     IF (error <= noHardware) & (error >= siUnknownQuality) THEN
  1594.         GetIndString(msg2, sSMErrStrings, ABS(error) + noHardware + 1)
  1595.     ELSE
  1596.         NumToString(error, msg2);
  1597.     ParamText(msg1, msg2, '', '');
  1598.     theItem:= CenteredAlert(rUserAlert);
  1599. END; {AlertUser}
  1600.  
  1601. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1602. {$S Main}
  1603. PROCEDURE EmergencyExit(message: INTEGER);
  1604.  
  1605. {Display an alert that tells the user an error occurred, then exit the
  1606.  program.  This routine is used as an ultimate bail-out for serious errors
  1607.  that prohibit the continuation of the application.  Errors that do not
  1608.  require the termination of the application are handled with AlertUser.
  1609.  Error checking and reporting has a place even in the simplest application.
  1610.  
  1611.  BUG NOTE: GetIndString will return a bogus string if the index is not
  1612.  positive.}
  1613.  
  1614. VAR
  1615.     msg1:                Str255;
  1616.     theItem:            INTEGER;
  1617.  
  1618. BEGIN
  1619.     SetCursor(qd.arrow);
  1620.     IF message > 0 THEN
  1621.         GetIndString(msg1, sErrStrings, message)
  1622.     ELSE
  1623.         msg1:= '';                                            {in case there is no message}
  1624.     ParamText(msg1, '', '', '');
  1625.     theItem:= CenteredAlert(rExitAlert);
  1626.     ExitToShell;                                            {we’re out of here}
  1627. END; {EmergencyExit}
  1628.  
  1629. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1630. {$S Main}
  1631. PROCEDURE Terminate;
  1632.  
  1633. {Clean up the application and exit.  I close all of the windows so that
  1634.  they can update their documents, if any.  Dispose of the Sound Unit and
  1635.  close the status window if the user really wants to quit.}
  1636.  
  1637. VAR
  1638.     aWindow:        WindowPtr;
  1639.     closed:        BOOLEAN;
  1640.  
  1641. BEGIN
  1642.     closed:= TRUE;
  1643.     KillSound;                                            {stop any sound in progress}
  1644.     REPEAT
  1645.         aWindow:= FrontWindow;                        {get the current front window}
  1646.         IF aWindow <> NIL THEN
  1647.             closed:= DoCloseWindow(aWindow);        {close this window}
  1648.     UNTIL (NOT closed) | (aWindow = NIL);        {do all windows}
  1649.     IF closed THEN BEGIN
  1650.         FreeSoundUnit;                                    {get rid of all sound equipment}
  1651.         ExitToShell;                                    {exit if no cancellation}
  1652.     END;
  1653. END; {Terminate}
  1654.  
  1655. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1656. {$S Main}
  1657. PROCEDURE DrawStatusWindow;
  1658.  
  1659. {Draw the status message and update the button.  I setup a rectangle that
  1660.  is the text area.  This is the entire window area, inset a small amount.
  1661.  I also subtract from the bottom of the rectangle the area used by the button.
  1662.  Doing things this way allowed me to change the size of the window and
  1663.  not change any of this code.  The rectangle used by text will fill the
  1664.  window regardless of the new size I give it in the WIND resource.}
  1665.  
  1666. VAR
  1667.     theRect:         Rect;
  1668.  
  1669. BEGIN
  1670.     PenNormal;
  1671.     WITH WindowPtr(gStatusWindow)^.portRect DO
  1672.         SetRect(theRect, left, top, right, bottom - kDafaultButSizeH);
  1673.     InsetRect(theRect, kMsgInset, kMsgInset);
  1674.     HLock(Handle(gStatusWindow^.message));
  1675.     TETextBox(Ptr(ORD(gStatusWindow^.message^) + 1),
  1676.                 LENGTH(gStatusWindow^.message^^), theRect, teJustCenter);
  1677.     HUnlock(Handle(gStatusWindow^.message));
  1678.     UpdateControls(WindowPtr(gStatusWindow), WindowPtr(gStatusWindow)^.visRgn);
  1679.     DoButtonOutline(WindowPeek(gStatusWindow)^.controlList);
  1680. END; {DrawStatusWindow}
  1681.  
  1682. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1683. {$S Main}
  1684. PROCEDURE ShowStatusWindow(messageID: INTEGER);
  1685.  
  1686. {Whenever I start a sound, I show this status window.  It will contain a
  1687.  message showing what is currently happening.  A very good rule is not to
  1688.  perform drawing outside of the update event.  I bend that rule here.  I
  1689.  could simply select this window and show it.  This would allow my normal
  1690.  drawing and activating routines to be called.  The problem with that was,
  1691.  I found that many times the status window appeared and then disappeared
  1692.  before these update routines had a chance to draw the window.  This left
  1693.  the user with a blank window that immediately went away.  So, I do the
  1694.  activation and drawing right after showing it.
  1695.  
  1696.  BUG NOTE: GetIndString will return a bogus string if the index is not
  1697.  positive.}
  1698.  
  1699. VAR
  1700.     msg:                Str255;
  1701.  
  1702. BEGIN
  1703.     IF FailLowMemory(0) THEN BEGIN                            {running low on memory?}
  1704.         KillSound;
  1705.         AlertUser(memFullErr, sLowMemory);
  1706.     END ELSE BEGIN
  1707.         IF messageID > 0 THEN
  1708.             GetIndString(msg, sMsgStrings, messageID)
  1709.         ELSE
  1710.             msg:= '';                                                {case there’s no message}
  1711.         SetString(gStatusWindow^.message, msg);
  1712.         ShowWindow(WindowPtr(gStatusWindow));                {show the window}
  1713.         SelectWindow(WindowPtr(gStatusWindow));            {bring it to the front}
  1714.         SetPort(GrafPtr(gStatusWindow));
  1715.         EraseRect(WindowPtr(gStatusWindow)^.portRect);    {erase the old message}
  1716.         HiliteControl(WindowPeek(gStatusWindow)^.controlList, kCntlActivate);
  1717.         DrawStatusWindow;                                            {draw the new message}
  1718.         ValidRect(WindowPtr(gStatusWindow)^.portRect);    {avoid a needless update}
  1719.         gStatusWindow^.showTime:= TickCount;                {it’s show time folks}
  1720.     END;
  1721. END; {ShowStatusWindow}
  1722.  
  1723. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1724. {$S Initialization}
  1725. PROCEDURE InitStatusWindow;
  1726.  
  1727. {This initializes the status window, and keeps it hidden until the user
  1728.  decides to play a sound.  I also center it for the first time, but the
  1729.  user is free to drag it to a new location afterwards.  The status window
  1730.  also has a message associated to it, so I allocate a string handle for this.}
  1731.  
  1732. VAR
  1733.     window:            WindowPtr;
  1734.     stopButton:        ControlHandle;
  1735.  
  1736. BEGIN
  1737.     gStatusWindow:= StatWindowPeek(NewPtrClear(SIZEOF(StatusWindow)));
  1738.     IF gStatusWindow = NIL THEN
  1739.         EmergencyExit(sInitStatusErr);
  1740.     window:= GetCenteredWindow(rStatusWindow, Ptr(gStatusWindow), Pointer(-1));
  1741.     IF window = NIL THEN
  1742.         EmergencyExit(sInitStatusErr);
  1743.     SetWRefCon(window, rStatusWindow);
  1744.     stopButton:= GetNewControl(rCancelCntl, window);
  1745.     IF stopButton = NIL THEN
  1746.         EmergencyExit(sInitStatusErr);
  1747.     gStatusWindow^.message:= NewString('');        {a new empty string handle}
  1748.     IF gStatusWindow^.message = NIL THEN
  1749.         EmergencyExit(sInitStatusErr);
  1750.     HNoPurge(Handle(gStatusWindow^.message));
  1751. END; {InitStatusWindow}
  1752.  
  1753. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1754. {$S Main}
  1755. PROCEDURE DoErrorSound(soundNo: INTEGER);
  1756.  
  1757. {I just flash the menubar for a moment instead of playing any sounds.  It’s
  1758.  the same effect as setting the sound volume to zero but that would prevent
  1759.  me playing my sounds.  Also, it helps to prevent the problem of _SysBeep
  1760.  being called by the Dialog Manager while I’m playing a sound.  If my
  1761.  application is playing a sound and, for example, the user clicks outside
  1762.  of the Standard File dialog ModalDialog calls _SysBeep.  This can cause
  1763.  the Mac to crash or trash my channel.
  1764.  
  1765.  BUG NOTE: If the current Sound Manager were playing a sound and a
  1766.  _SysBeep were to occur, bad things could happen on a Mac Plus/SE.  Either
  1767.  the application’s channel would be trashed or the Mac could crash.
  1768.  
  1769.  VERSION 1.1: The new Sound Manager will handle the problem presented in the 
  1770.  older Sound Manager regarding SysBeep.  If I'm running under the new Sound 
  1771.  Manager then I'll call SysBeep anyway.  The new Sound Manager will properly 
  1772.  handle the situation of an open sound channel when SysBeep is called.}
  1773.  
  1774. VAR
  1775.     finalTicks:        LongInt;
  1776.  
  1777. BEGIN
  1778.     IF HasNewSndMgr THEN
  1779.         SysBeep(30)                                            {does the right thing now}
  1780.     ELSE BEGIN
  1781.         IF soundNo > 0 THEN BEGIN                        {only after the first time}
  1782.             FlashMenuBar(0);
  1783.             Delay(kDelayTime, finalTicks);
  1784.             FlashMenuBar(0);
  1785.         END;
  1786.     END;
  1787. END;
  1788.  
  1789. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1790. {$S Initialization}
  1791. PROCEDURE CheckSndVol;
  1792.  
  1793. {This tests if the volume has been set very low.  It’s a judgement call as
  1794.  to what “too low” means.  The user sets the volume, so how could it be too
  1795.  low? A setting of 0 could be considered too low, but in any case do not
  1796.  adjust it.  I find the volume goes below 4 on a standard Mac, then there
  1797.  is a perceivable difference.  I also wanted to show this routine as a
  1798.  demonstration in how to handle such a situation.  Do not change it
  1799.  directly! Ask the user to do it.  Consider the user that has their Mac
  1800.  connected to a stack of Marshalls (which go to 11) and has purposely set
  1801.  their Mac’s volume where they wanted it.  This may be 3 and if you crack
  1802.  it up to 8, it could be a scene out of Back to the Future.
  1803.  
  1804.  User Interface rule, let the user remain in control!}
  1805.  
  1806. VAR
  1807.     curVol,
  1808.     item:            INTEGER;
  1809.     numStr:        Str255;
  1810.  
  1811. BEGIN
  1812.     GetSoundVol(curVol);
  1813.     IF (curVol < kMinVolumeDesired) THEN BEGIN
  1814.         NumToString(curVol, numStr);
  1815.         ParamText(numStr, '', '', '');                    {show the current volume}
  1816.         item:= CenteredAlert(rSoundVolAlert);
  1817.     END;
  1818. END;
  1819.  
  1820. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1821. {$S Open}
  1822. PROCEDURE AddSndDocControls(window: WindowPtr);
  1823.  
  1824. {Simply add all the buttons I want to use in the document window.  I get
  1825.  the control templates, and add the control to the window.  I don’t check
  1826.  for getting errors for two reasons.  I put these controls there.  If the
  1827.  user removes them, it’s their problem.  If the application cannot get the
  1828.  memory required for these, then the application is already out of memory
  1829.  and checking here probably wouldn’t help much.  I do check before creating
  1830.  the document that there was enough available memory.  I save the resource
  1831.  ID into the control’s refCon so later I can tell which control was hit by
  1832.  the user.
  1833.  
  1834.  VERSION 1.1: Add the record button to the window.}
  1835.  
  1836. VAR
  1837.     control:                    ControlHandle;
  1838.  
  1839. BEGIN
  1840.     control:= GetNewControl(rPlaySndCntl, window);
  1841.     IF control <> NIL THEN
  1842.         SetControlReference(control, rPlaySndCntl);
  1843.     control:= GetNewControl(rHyperPlayCntl, window);
  1844.     IF control <> NIL THEN
  1845.         SetControlReference(control, rHyperPlayCntl);
  1846.     control:= GetNewControl(rPlayScaleCntl, window);
  1847.     IF control <> NIL THEN
  1848.         SetControlReference(control, rPlayScaleCntl);
  1849.     control:= GetNewControl(rMelodyCntl, window);
  1850.     IF control <> NIL THEN
  1851.         SetControlReference(control, rMelodyCntl);
  1852.     control:= GetNewControl(rStopCntl, window);
  1853.     IF control <> NIL THEN
  1854.         SetControlReference(control, rStopCntl);
  1855.     control:= GetNewControl(rRecordCntl, window);
  1856.     IF control <> NIL THEN
  1857.         SetControlReference(control, rRecordCntl);
  1858. END;
  1859.  
  1860. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1861. {$S Open}
  1862. FUNCTION NewStackedWindow(windID: INTEGER; windStorage: Ptr): WindowPtr;
  1863.  
  1864. {There are a few approaches I thought of and tried, none of which satisfied
  1865.  me.  ResEdit uses the simplest method, an offset from the front window.  I
  1866.  don’t like this because it may cover up existing windows.  So my approach is
  1867.  to go through the window list looking for an empty spot.  If the empty
  1868.  spot causes the window to go off the screen, then I start a new stack off
  1869.  to the right.  This will cause a new series of window to be stacked.  If the
  1870.  new stack would cause the window to go off screen, then it’s time to give up.
  1871.  All new windows will then be moved to the starting point and there is no
  1872.  further stacking.  If I look at this routine too much, I get real tired.
  1873.  If I try to change it at all, I get woozy trying to make it work again.}
  1874.  
  1875. CONST
  1876.     kStartPt =            2;            {offset from the topLeft of the screen}
  1877.     kStaggerH =            16;        {staggering amounts for new windows}
  1878.     kStaggerV =            3;            {not including the window’s title height}
  1879.  
  1880. VAR
  1881.     windSize, newPt,
  1882.     oldPt, delta:        Point;
  1883.     newWindow,
  1884.     oldWindow:            WindowPtr;
  1885.     wTitleHeight,
  1886.     numStacks:            INTEGER;
  1887.     taken:                BOOLEAN;
  1888.  
  1889.     FUNCTION PositionAvailable: BOOLEAN;
  1890.     BEGIN
  1891.         taken:= FALSE;
  1892.         oldWindow:= FrontWindow;
  1893.         WHILE (oldWindow <> NIL) & NOT taken DO BEGIN
  1894.             IF IsDocWindow(oldWindow) & WindowPeek(oldWindow)^.visible THEN BEGIN
  1895.                 oldPt:= GetGlobalTopLeft(oldWindow);
  1896.                 delta.v:= ABS(newPt.v - oldPt.v);
  1897.                 delta.h:= ABS(newPt.h - oldPt.h);
  1898.                 taken:= (delta.h + delta.v)
  1899.                           <= ((kStaggerH + kStaggerV + wTitleHeight) DIV 2);
  1900.             END;
  1901.             oldWindow:= WindowPtr(WindowPeek(oldWindow)^.nextWindow);
  1902.         END;
  1903.         PositionAvailable:= NOT taken;
  1904.     END;
  1905.  
  1906.     FUNCTION OutOfBounds: BOOLEAN;
  1907.     BEGIN
  1908.         OutOfBounds:= ((newPt.v + windSize.v) > qd.screenBits.bounds.bottom)
  1909.                          | ((newPt.h + windSize.h) > qd.screenBits.bounds.right);
  1910.     END;
  1911.  
  1912. BEGIN {set the initail starting point for a window, less the staggering amount}
  1913.     newWindow:= GetNewWindow(windID, windStorage, WindowPtr(-1));
  1914.     windSize:= newWindow^.portRect.botRight;
  1915.     SubPt(newWindow^.portRect.topLeft, windSize);
  1916.     wTitleHeight:= GetWTitleHeight(GetWVariant(newWindow));
  1917.     SetPt(newPt, kStartPt - kStaggerH, kStartPt - kStaggerV + GetMBarHeight);
  1918.  
  1919.     REPEAT {add the staggering amount then test if the window goes off the screen}
  1920.         taken:= TRUE;
  1921.         SetPt(newPt, newPt.h + kStaggerH, newPt.v + kStaggerV + wTitleHeight);
  1922.         IF OutOfBounds THEN BEGIN
  1923.              numStacks:= numStacks + 1;
  1924.             SetPt(newPt, kStartPt + (numStacks * kStaggerH),
  1925.                               kStartPt + wTitleHeight + GetMBarHeight);
  1926.             IF OutOfBounds THEN BEGIN
  1927.                 SetPt(newPt, kStartPt, kStartPt + wTitleHeight + GetMBarHeight);
  1928.                 taken:= FALSE;
  1929.             END;
  1930.         END;
  1931.     UNTIL NOT taken | PositionAvailable;
  1932.  
  1933.     MoveWindow(newWindow, newPt.h, newPt.v, TRUE);
  1934.     NewStackedWindow:= newWindow;
  1935. END; {NewStackedWindow}
  1936.  
  1937. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  1938. {$S Open}
  1939. FUNCTION CreateSoundDoc(resRef: INTEGER; fNamePtr: StringPtr;
  1940.                             vRefNum: INTEGER; dirID: LONGINT): OSErr;
  1941.  
  1942. {Create a sound document and initialize all the data associated with
  1943.  it.  If I encounter an error, then dispose of any memory allocated in the
  1944.  attempt.  Allocate all necessary memory required for a sound document.  I
  1945.  like to use NewPtrClear because is will initialize all the memory.  I
  1946.  depend on real values or zero in my document fields.  Then its time to
  1947.  build a sound document which contains all the controls and the list of
  1948.  sounds.  I get the position for the list kept in a rectangle resource.
  1949.  This  helps me to adjust the size of the list without changing code.  I
  1950.  will make  an additional adjustment to the list’s height to insure better
  1951.  scrolling.  This avoids the ugly white space of an improperly sized
  1952.  rectangle.  If I  cannot create the list, I’ll close the window and return
  1953.  a NIL.  If I find  after creating a new document that I’m low on memory,
  1954.  I’ll close it and  return an error.
  1955.  
  1956.  SPECIAL NOTE: The Human Interface Group suggested that the buttons and the
  1957.  list should in the System font.  I designed these windows using the
  1958.  application font in 10 point.  If you change the call to TextFont, you'll
  1959.  find that the buttons and the list will work properly in your font choice.
  1960.  
  1961.  VERSION 1.1:  Size the window smaller to hide the record button from the
  1962.  user if Sound Input is not available.  Added memory check to the beginning
  1963.  of this routine before creating the document.  This was done because there
  1964.  are two routines that call this one, OpenSoundDoc and NewSoundDoc which
  1965.  both performed this check.  Having here in one place reduces the chance of
  1966.  future bugs (doing the same thing in two places) and reduces the size of
  1967.  the code.}
  1968.  
  1969. VAR
  1970.     newDocPtr:                SndDocPeek;
  1971.     window:                    WindowPtr;
  1972.     list:                        ListHandle;
  1973.     rHandle:                    RectHandle;
  1974.     lView, lBounds:        Rect;
  1975.     aCell:                    Cell;
  1976.     cSize, newSize:        Point;
  1977.     maxListHeight:            INTEGER;
  1978.     theErr:                    OSErr;
  1979.     ignore:                    BOOLEAN;
  1980.  
  1981. BEGIN
  1982.     IF FailLowMemory(kMemForSndDoc) THEN BEGIN
  1983.         CloseResFile(resRef);                            {close it before leaving}
  1984.         CreateSoundDoc:= memFullErr;
  1985.         EXIT(CreateSoundDoc);
  1986.     END;
  1987.     newDocPtr:= SndDocPeek(NewPtrClear(SIZEOF(SndDocument)));
  1988.     theErr:= MemError;
  1989.     IF newDocPtr <> NIL THEN BEGIN
  1990.         window:= NewStackedWindow(rSoundWindow, Ptr(newDocPtr));
  1991.         SetPort(window);
  1992.         IF HasSoundInput THEN
  1993.             SizeWindow(window, kSoundWindowSizeW, kSoundWindowSizeH, FALSE);
  1994.         TextFont(0);                                        {set window to System font, blech}
  1995.         AddSndDocControls(window);                        {add my buttons}
  1996.         SetWRefCon(WindowPtr(newDocPtr), rSoundWindow); {mark as an app window}
  1997.         SetWTitle(WindowPtr(newDocPtr), fNamePtr^);
  1998.         newDocPtr^.resFile:= resRef;                    {save its resource file ref}
  1999.         newDocPtr^.vRefNum:= vRefNum;                    {save its volume reference}
  2000.         newDocPtr^.dirID:= dirID;                        {save its directory ID}
  2001.         newDocPtr^.sndInUse:= FALSE;                    {not yet it doesn’t}
  2002.         rHandle:= RectHandle(Get1Resource('RECT', rListRectID));
  2003.         IF rHandle <> NIL THEN                            {get the stored list size}
  2004.             lView:= rHandle^^;
  2005.         SetRect(lBounds, 0, 0, 1, 0);                    {one dimentional list}
  2006.         SetPt(cSize, 0, 0);                                {List Mgr will find cell size}
  2007.         list:= LNew(lView, lBounds, cSize, 0, window, FALSE, FALSE, FALSE, TRUE);
  2008.         WITH window^.portRect DO
  2009.             maxListHeight:= bottom - top - (2 * kSndStdSpacing);
  2010.         IF maxListHeight < (lView.bottom - lView.top) THEN
  2011.             maxListHeight:= (lView.bottom - lView.top);
  2012.         IF list <> NIL THEN BEGIN
  2013.             newSize:= list^^.cellSize;                    {get the size of one cell}
  2014.             WHILE (newSize.v + list^^.cellSize.v) < maxListHeight DO
  2015.                 newSize.v:= newSize.v + list^^.cellSize.v;
  2016.             LSize(newSize.h, newSize.v, list);        {adjust for best scrolling}
  2017.             list^^.selFlags:= lOnlyOne;                {single selections only}
  2018.             newDocPtr^.list:= list;                        {save the list handle}
  2019.             theErr:= InitSndList(newDocPtr);            {initialize the list data}
  2020.             IF theErr = noErr THEN BEGIN
  2021.                 SetPt(aCell, 0, 0);                        {by default, I will...}
  2022.                 LSetSelect(TRUE, aCell, newDocPtr^.list); {select first item}
  2023.                 LSetDrawingMode(TRUE, newDocPtr^.list);
  2024.                 ShowWindow(WindowPtr(newDocPtr));    {get the show on the road}
  2025.                 IF FailLowMemory(0) THEN                {if I’m low on memory...}
  2026.                     theErr:= memFullErr;
  2027.             END;
  2028.         END ELSE
  2029.             theErr:= nilHandleErr;                        {list handle was NIL}
  2030.         IF theErr <> noErr THEN BEGIN                    {could not create the list}
  2031.             ignore:= DoCloseWindow(window);            {if not, close the window}
  2032.             newDocPtr:= NIL;                                {return NIL pointer too}
  2033.         END;
  2034.     END;
  2035.     CreateSoundDoc:= theErr;                            {return the error, if any}
  2036. END; {CreateSoundDoc}
  2037.  
  2038. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2039. {$S Open}
  2040. PROCEDURE OpenSoundDoc(fNamePtr: StringPtr; wdVRefNum: INTEGER);
  2041.  
  2042. {Believe it or not, but this turned out to be one of the more difficult
  2043.  routines to write.  This was the best approach I could thing of to avoid
  2044.  serious problems.  I don’t center the Standard File dialog because it
  2045.  wouldn’t be under the File menu where the user probably has the mouse.  I
  2046.  use OpenRFPerm because it allows for permissions and doesn’t depend on the
  2047.  current directory.  First problem with opening a resource file was to
  2048.  check if the file already being open.  Read Tech Notes #116 and #185.
  2049.  GetFInfo seems to do the trick.  At least it does report all files that
  2050.  are open on this machine.  It may be incorrect for files on AppleShare
  2051.  opened by other machines.  What I’m worried about is when opening a
  2052.  resource file for the second time on the same machine may return the
  2053.  previous opener’s resource map.  This is very dangerous, and if the second
  2054.  opener calls CloseResFile there will be a crash.  I want to be friendly
  2055.  and if the user tries to open the file the second time and I’ve already
  2056.  got it on the screen, then I’ll bring that window forward.  To test for
  2057.  this, I compare the file’s name, DirID and vRefNum.  The vRefNum is
  2058.  dynamic and needs to be converted into a volume name if I were to save
  2059.  this information for future use.
  2060.  
  2061.  There are a couple pitfalls, even with this method.  The user can open the
  2062.  file, then move it to another directory.  This same thing confuses all
  2063.  other applications I’ve tested.  Opening a resource file will allocate a
  2064.  resource map handle into my heap, which may be large depending on the
  2065.  number of resources in that file.  Additionally this loads all resources
  2066.  marked “preload.”  So I set ResLoad to FALSE to prevent the preloading.  I
  2067.  test if the amount of memory I believe my document will require is
  2068.  available after opening the resource file.  If true, I test if there are
  2069.  any sound resources in that file and if not alert the user.  After all
  2070.  this, create the new document.  If after creating the new document I find
  2071.  that memory is too low, will close it and return an error.  If it does
  2072.  fail, then close the file before anything else or there may not be enough
  2073.  memory to show the alert.  All of these tests created a number of
  2074.  IF-THEN-ELSE blocks and became unyielding.  C programmers get a break, so
  2075.  gimme me one too.  I use the MPW Pascal EXIT.
  2076.  
  2077.  BUG NOTE: Don’t open a resource file that is already open.  OpenResFile
  2078.  may return an existing resource map when it gets opWrErr from the file
  2079.  system.  If this happens, the resource file will not be unique and this
  2080.  is very bad.  Another problem is if I get a read-only path and someone
  2081.  else opens it for read/write.  This is also very bad.  Read Tech Notes
  2082.  #116 and #185 hint at this problem, but I think a more comprehensive one
  2083.  is in order.
  2084.  
  2085.  BUG NOTE: GetWDInfo fails with nsvErr if the working directory returned
  2086.  from Standard File is the root of an A/UX volume.  I could work around
  2087.  this,  but it would be dependant on the current version of A/UX.  Read
  2088.  Tech Note #229.  I believe this is also true for TOPS.
  2089.  
  2090.  VERSION 1.1:  I'll open files that do not have snd resources in them.  
  2091.  This allows users to open existing files and then record or paste a sound 
  2092.  into it.  I changed OpenRFPerm to HOpenResFile to avoid working 
  2093.  directories and to be consistent with the rest of the sources.  I 
  2094.  separated the standard file code from OpenSndDoc to support opening 
  2095.  documents being open from the Finder.  This allows me to share the same 
  2096.  routine to open files either from double clicking them in the Finder, or 
  2097.  by the Standard File dialog.  Removed the checking for low memory conditions
  2098.  since CreateSoundDoc is now doing this.}
  2099.  
  2100. VAR
  2101.     fPBRec:            ParamBlockRec;
  2102.     window:            WindowPtr;
  2103.     dirID, procID:    LONGINT;
  2104.     resFileRef,
  2105.     vRefNum,
  2106.     numSnds:            INTEGER;
  2107.     theErr:            OSErr;
  2108.  
  2109. BEGIN
  2110.     WITH fPBRec DO BEGIN                                {prepare a paramBlock}
  2111.         ioCompletion:= NIL;
  2112.         ioNamePtr:= fNamePtr;
  2113.         ioVRefNum:= wdVRefNum;
  2114.         ioFVersNum:= 0;
  2115.         ioFDirIndex:= 0;
  2116.     END;                                                    {get file location}
  2117.     theErr:= GetWDInfo(wdVRefNum, vRefNum, dirID, procID);
  2118.     IF theErr <> noErr THEN BEGIN
  2119.         AlertUser(theErr, sStandardErr);            {this fails at the root on A/UX}
  2120.         Exit(OpenSoundDoc);
  2121.     END;
  2122.     theErr:= PBGetFInfo(@fPBRec, NOT kFSAsynch); {fPBRec on stack, synch only}
  2123.     IF theErr = noErr THEN BEGIN
  2124.         IF BTst(fPBRec.ioFlAttrib, kResForkOpenBit) THEN BEGIN
  2125.             IF OpenByApp(vRefNum, dirID, fNamePtr, window) THEN
  2126.                 SelectWindow(window)                    {I opened this one, select it}
  2127.             ELSE
  2128.                 AlertUser(noErr, sCurInUseErr);    {in use by someone else}
  2129.             Exit(OpenSoundDoc);
  2130.         END;
  2131.     END ELSE BEGIN
  2132.         AlertUser(theErr, sStandardErr);            {PBGetFInfo failed}
  2133.         Exit(OpenSoundDoc);
  2134.     END;
  2135.     SetResLoad(FALSE);                                {don’t load any resources}
  2136.     resFileRef:= HOpenResFile(vRefNum, dirID, fNamePtr^, fsCurPerm);
  2137.     theErr:= ResError;                                {save error, if any}
  2138.     SetResLoad(TRUE);                                    {restore ResLoad state}
  2139.     UseResFile(gAppResRef);                            {changes ResErr}
  2140.     IF resFileRef <> -1 THEN                        {error from OpenRFPerm?}
  2141.         theErr:= CreateSoundDoc(resFileRef, fNamePtr, vRefNum, dirID);
  2142.     IF theErr <> noErr THEN
  2143.         AlertUser(theErr, sNewDocErr);             {couldn’t create new doc}
  2144. END; {OpenSoundDoc}
  2145.  
  2146. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2147. {$S Open}
  2148. PROCEDURE NewSoundDoc;
  2149.  
  2150. {VERSION 1.1:  Create a new file for the user.  I prefer to use the new 
  2151.  HCreateResFile to avoid working directories, and to be consistent with 
  2152.  the rest of the sources.  Also, I need the real vRefNum and dirID to 
  2153.  store in the sndDoc record.  After creating the file, I set its Finder 
  2154.  information to the proper type and creator.  Finally, I get to open the 
  2155.  new file.  After this point everything is exactly like opening an 
  2156.  existing file and creating a new sound document.}
  2157.  
  2158. VAR
  2159.     msg:                Str255;
  2160.     reply:            SFReply;
  2161.     fndrInfo:        FInfo;
  2162.     strHandle:        StringHandle;
  2163.     dirID:            LONGINT;
  2164.     procID:            LONGINT;
  2165.     vRefNum:            INTEGER;
  2166.     resFileRef:        INTEGER;
  2167.     theErr:            OSErr;
  2168.  
  2169. BEGIN
  2170.     theErr:= noErr;
  2171.     strHandle:= StringHandle(Get1Resource('STR ', rPutFileMsg));
  2172.     IF strHandle <> NIL THEN
  2173.         msg:= strHandle^^                                        {save no name title}
  2174.     ELSE
  2175.         msg:= '';                                                {at least an empty string}
  2176.     SFPutFile(Point(kSFTopLeft), msg, '', NIL, reply);
  2177.  
  2178.     IF reply.good THEN BEGIN
  2179.         theErr:= GetWDInfo(reply.vRefNum, vRefNum, dirID, procID);
  2180.         HCreateResFile(vRefNum, dirID, reply.fName);
  2181.         theErr:= ResError;
  2182.         IF theErr = noErr THEN BEGIN
  2183.             theErr:= HGetFInfo(vRefNum, dirID, reply.fName, fndrInfo);
  2184.             IF theErr = noErr THEN BEGIN
  2185.                 fndrInfo.fdType:= rSndAppDocType;
  2186.                 fndrInfo.fdCreator:= rAppSignature;
  2187.                 theErr:= HSetFInfo(vRefNum, dirID, reply.fName, fndrInfo);
  2188.                 resFileRef := HOpenResFile(vRefNum, dirID, reply.fName, fsCurPerm);
  2189.                 theErr:= ResError;                            {save error, if any}
  2190.                 UseResFile(gAppResRef);                        {changes ResErr}
  2191.                 IF resFileRef <> -1 THEN                    {error from HOpenResFile}
  2192.                     theErr:= CreateSoundDoc(resFileRef, @reply.fName, vRefNum, dirID);
  2193.             END;
  2194.         END;
  2195.     END;
  2196.     IF theErr <> noErr THEN
  2197.         AlertUser(theErr, sNewDocErr);                     {return the error}
  2198. END;
  2199.  
  2200. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2201. {$S Open}
  2202. FUNCTION SFFilter(p: CInfoPBPtr): BOOLEAN;
  2203.  
  2204. {This is much more than what a typical file filter might do but I wanted
  2205.  the user to easily find files that contained snd resources.  So, the
  2206.  global flag gSndFilesOnly is used to determine when to filter for such
  2207.  files.  Otherwise, I show all files and allow the application to report to
  2208.  the user the file they’ve just tried to open doesn’t have any sounds to
  2209.  play with.  Opening resource forks can be tricky if it’s already open.  It
  2210.  would be very bad to use a resource fork that is already open by another
  2211.  application.  The problem being that the Resource Manager doesn’t deal
  2212.  with multiple users.  Read Tech notes #116 and #185.  There’s also another
  2213.  problem.  If a resource fork is opened by two applications and one closes
  2214.  the file then the entire resource fork may closed out from underneath the
  2215.  other application.  I do, however, want to show the user files that
  2216.  contain sound resources even if they are currently open.  I use
  2217.  HOpenResFile with read only permission, which will give me a unique
  2218.  resource reference.  I look for a 'snd ' resource and then immediately
  2219.  close the file.  Do not use a resource file opened with read only
  2220.  permission.  Another reason to use HOpenResFile is to avoid a necessary
  2221.  working directory.  I do not have one while in the file filter, but can
  2222.  use the DirID.  Performing this search on each file is time consuming so I
  2223.  show a spinning cursor to show the user I’m working.   Opening a resource
  2224.  fork may load resources mark preload.   To avoid this, I call SetResLoad
  2225.  to FALSE.  I bet you thought the Resource Manager was a free lunch.  Ha!
  2226.  Read Tech Note #203 for other reasons not to play with resources.
  2227.  
  2228.  BUG NOTE: While debugging this routine using heap scramble, I found that
  2229.  OpenResFile would not open the requested file.  I’m not sure if this is a
  2230.  problem with OpenResFile or SFGetFile.
  2231.  
  2232.  VERSION 1.1: The bug mentioned above was found.  The problem is that the 
  2233.  paramBlock happens to be a re-locatable block in the heap.  Passing the 
  2234.  de-reference ioNamePtr to HOpenResFile was de-referencing this 
  2235.  re-locatable paramBlock.  Since HOpenResFile moves memory, the namePtr 
  2236.  would no longer valid and thus HOpenResFile would fail.  Now I copy the 
  2237.  name out of the paramBlock and use the local variable in HOpenResFile.  
  2238.  This problem was fixed in System 7, which no longer passes a re-locatable 
  2239.  block to the file filter.  The new version allows for any resource file 
  2240.  to be opened  Because of this new feature, I only show the user files 
  2241.  that have a resource fork.}
  2242.  
  2243. CONST
  2244.     kShowIt =            FALSE;            {FALSE means I do not filter out...}
  2245.     kDoNotShowIt =        TRUE;                {the file and TRUE means that I do.}
  2246.  
  2247. VAR
  2248.     resFName:            Str255;
  2249.     oldTicks:            LONGINT;
  2250.     resRef, curRes:    INTEGER;
  2251.     theErr:                OSErr;
  2252.  
  2253. BEGIN
  2254.     oldTicks:= TickCount;
  2255.     SFFilter:= kDoNotShowIt;            {don’t show anything until I say so}
  2256.     IF gSndFilesOnly THEN BEGIN
  2257.         curRes:= CurResFile;
  2258.         resFName:= p^.ioNamePtr^;
  2259.         SetResLoad(FALSE);
  2260.         resRef:= HOpenResFile(p^.ioVRefNum, LMGetCurDirStore, resFName, fsRdPerm);
  2261.         IF (resRef <> -1) THEN BEGIN
  2262.             UseResFile(resRef);
  2263.             IF Count1Resources('snd ') > 0 THEN
  2264.                 SFFilter:= kShowIt;            {hey, we found a sound in here}
  2265.             CloseResFile(resRef);
  2266.         END;                                        {restore everything}
  2267.         SetResLoad(TRUE);
  2268.         UseResFile(curRes);
  2269.     END ELSE BEGIN
  2270.         IF p^.ioFlRLgLen > 0 THEN
  2271.             SFFilter:= kShowIt;
  2272.     END;
  2273.     RotateCursor(TickCount - oldTicks);
  2274. END; {SFFilter}
  2275.  
  2276. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2277. {$S Open}
  2278. FUNCTION SFGetHook(MySFItem: INTEGER; dialog: DialogPtr): INTEGER;
  2279.  
  2280. CONST
  2281.     kFirstTime =     -1;                    {item is -1 first time into hook}
  2282.     reDrawList =    101;                    {returning 101 as item number will
  2283.                                                  cause the file list to redraw}
  2284. VAR
  2285.     kind:                INTEGER;
  2286.     cntl:                ControlHandle;
  2287.     r:                    Rect;
  2288.  
  2289. BEGIN
  2290.     SFGetHook := MySFitem;
  2291.     GetDialogItem(dialog, rSndOnlyCheckBox, kind, Handle(cntl), r);
  2292.     CASE MySFItem OF
  2293.  
  2294.         kFirstTime: BEGIN
  2295.             IF gSndFilesOnly THEN
  2296.                 SetControlValue(cntl, kCntlOn)
  2297.             ELSE
  2298.                 SetControlValue(cntl, kCntlOff);
  2299.         END;
  2300.  
  2301.         rSndOnlyCheckBox: BEGIN
  2302.             IF GetControlValue(cntl) = kCntlOff THEN BEGIN
  2303.                 SetControlValue(cntl, kCntlOn);
  2304.                 gSndFilesOnly:= TRUE;
  2305.             END ELSE BEGIN
  2306.                 SetControlValue(cntl, kCntlOff);
  2307.                 gSndFilesOnly:= FALSE;
  2308.             END;
  2309.             SFGetHook:= reDrawList;
  2310.         END;
  2311.     END;
  2312. END; {SFGetHook}
  2313.  
  2314. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2315. {$S Main}
  2316. PROCEDURE GetSoundDoc;
  2317.  
  2318. VAR
  2319.     reply:             SFReply;
  2320.     fileTypes:        SFTypeList;                            {not used, just a placeholder}
  2321.  
  2322. {VERSION 1.1: This routine used to be inside of OpenSoundDoc.  The 
  2323.  standard file code was removed to here in order to support opening 
  2324.  documents being open from the Finder.  I've switched to using a constant 
  2325.  for the topLeft point of the Standard File dialog.  It saves a few bytes 
  2326.  of code, so what the hell?  I found a cosmetic problem with the cursor 
  2327.  not being restored back to arrow after returning Standard File if there 
  2328.  was an error dialog shown immediately.  So, it is immediately set back to 
  2329.  the arrow.}
  2330.  
  2331. VAR
  2332.     ffUPP:        FileFilterUPP;
  2333.     dhUPP:        DlgHookUPP;
  2334.  
  2335. BEGIN
  2336.     SpinCursor(0);                                            {get the spinning cursor ready}
  2337.     
  2338.     ffUPP := NewFileFilterProc(@SFFilter);
  2339.     dhUPP := NewDlgHookProc(@SFGetHook);
  2340.     SFPGetFile(Point(kSFTopLeft), '',
  2341.                ffUPP, -1, fileTypes,
  2342.                dhUPP, reply, rSFPGetFileDLOG, NIL);
  2343.     DisposeRoutineDescriptor(ffUPP);
  2344.     DisposeRoutineDescriptor(dhUPP);
  2345.  
  2346.     SetCursor(qd.arrow);
  2347.     IF reply.good THEN
  2348.         OpenSoundDoc(@reply.fName, reply.VRefNum);
  2349. END; {GetSoundDoc}
  2350.  
  2351. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2352. {$S Main}
  2353. PROCEDURE DrawAboutWindow(window: WindowPtr);
  2354.  
  2355. {Draw the contents of the about window in response to an update event.  At
  2356.  this point, BeginUpdate has been called which sets the window’s visRgn to
  2357.  clip drawing only where it needs to be done.  I have some text to draw, an
  2358.  icon, a picture, and a default button with its outline.  If I have a color
  2359.  icon handle I’ll call PlotCIcon.  I offset the picture to draw to the
  2360.  right of the icon.  The text will appear in a rectangle as large as the
  2361.  window, but below the icon and above the button.  This allows me the
  2362.  change the text and if needed change the size of the window’s rectangle to
  2363.  compensate.  I won’t have to recompile any code.  Some people use dialogs
  2364.  because of this, but I’m demonstrating how it can be done without them.  I
  2365.  use UpdateControls to avoid needless drawing that happens with DrawControls.
  2366.  It not only runs faster but doesn’t flicker.}
  2367.  
  2368. VAR
  2369.     theRect:            Rect;
  2370.  
  2371. BEGIN
  2372.     PenNormal;
  2373.     SetRect(theRect, kIconLeft, kIconTop, kIconRight, kIconBot);
  2374.     IF AboutWPeek(window)^.iconIsColor THEN
  2375.         PlotCIcon(theRect, CIconHandle(AboutWPeek(window)^.appIcon))
  2376.     ELSE
  2377.         PlotIcon(theRect, AboutWPeek(window)^.appIcon);
  2378.  
  2379.     theRect:= PicHandle(AboutWPeek(window)^.appPict)^^.picFrame;
  2380.     OffsetRect(theRect, -theRect.left + kIconRight + kIconPictGap,
  2381.                               -theRect.top + kIconTop);
  2382.     DrawPicture(PicHandle(AboutWPeek(window)^.appPict), theRect);
  2383.  
  2384.     WITH window^.portRect DO
  2385.         SetRect(theRect, left, kIconBot, right, bottom - kDafaultButSizeH);
  2386.     InsetRect(theRect, kMsgInset, kMsgInset);
  2387.     HLock(AboutWPeek(window)^.comment);
  2388.     TETextBox(Ptr(ORD(AboutWPeek(window)^.comment^) + 1),
  2389.                 LENGTH(StringHandle(AboutWPeek(window)^.comment)^^),
  2390.                 theRect, teJustCenter);
  2391.     HUnlock(AboutWPeek(window)^.comment);
  2392.  
  2393.     UpdateControls(window, window^.visRgn);
  2394.     DoButtonOutline(WindowPeek(window)^.controlList);
  2395. END; {DrawAboutWindow}
  2396.  
  2397. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2398. {$S Main}
  2399. PROCEDURE DoAbout;
  2400.  
  2401. {First thing to do is call KillSound to stop any sound in progress and
  2402.  close the status window.  If there’s enough memory available, I’ll play
  2403.  the about sound.  This is sound is asynchronous and will automatically be
  2404.  disposed of when completed.  There is an icon, taken from the bundle
  2405.  resources, and a picture.  The text is taken from a string resource.  It
  2406.  contains a couple variables just like ParamText would want.  I get the
  2407.  current name of the application from the AppParms.  This is the name of
  2408.  the application as specified by the user.  I read in my 'vers' resource
  2409.  to get the current version to be displayed in the window.  Read Tech Note
  2410.  189 for more details on the vers resource.  I think these are two
  2411.  important things to show in the about box.  These two portions of the text
  2412.  are put into place with my own version of ParamText.   I tried all of this
  2413.  with a standard dialog, but had trouble.  Sometimes the color icon didn’t
  2414.  show up.  Trying to center justify text with a statText item wasn’t really
  2415.  possible.  On the Mac SE, for some unknown reason, TESetAlignment failed to
  2416.  center the textH of the dialog.  I also found that this call would set the
  2417.  dialog’s text back to Chicago even after I had called TextFont.  So, I
  2418.  gave up and did everything myself.  This is a demonstration of how to
  2419.  create a dialog without using the Dialog Manager.}
  2420.  
  2421. VAR
  2422.     verNum,
  2423.     userName:        Str255;
  2424.     aboutPtr:        Ptr;
  2425.     aboutPeek:        AboutWPeek;
  2426.     control:            ControlHandle;
  2427.     hndl:                Handle;
  2428.     curVersion:        VersRecHndl;
  2429.     refNum:            INTEGER;
  2430.     theErr:            OSErr;
  2431.     ignore:            BOOLEAN;
  2432.  
  2433. BEGIN
  2434.     KillSound;
  2435.     aboutPtr:= NewPtrClear(SIZEOF(AboutWindow));
  2436.     IF aboutPtr <> NIL THEN BEGIN
  2437.         aboutPeek:= AboutWPeek(GetCenteredWindow(rAboutWindow, aboutPtr, WindowPtr(-1)));
  2438.         SetWRefCon(WindowPtr(aboutPeek), rAboutWindow);
  2439.         curVersion:= VersRecHndl(Get1Resource('vers', 1));
  2440.         IF curVersion <> NIL THEN
  2441.             verNum:= curVersion^^.shortVersion                {get version string}
  2442.         ELSE
  2443.             verNum:= '';                                            {at least initialize it}
  2444.         GetAppParms(userName, refNum, hndl);
  2445.         aboutPeek^.appPict:= Get1Resource('PICT', rAppPict);
  2446.         aboutPeek^.comment:= Get1Resource('STR ', rAboutText);
  2447.         aboutPeek^.appIcon:= NIL;
  2448.         aboutPeek^.iconIsColor:= TRUE;
  2449.         IF gMac.hasColorQD THEN
  2450.             aboutPeek^.appIcon:= Handle(GetCIcon(rMoofIcon));
  2451.         IF aboutPeek^.appIcon = NIL THEN BEGIN
  2452.             aboutPeek^.appIcon:= Get1Resource('ICON', rMoofIcon);
  2453.             aboutPeek^.iconIsColor:= FALSE;
  2454.         END;
  2455.         control:= GetNewControl(rAboutOkCntl, WindowPtr(aboutPeek));
  2456.  
  2457.         IF (aboutPeek^.appPict <> NIL) | (aboutPeek^.comment <> NIL)
  2458.          | (aboutPeek^.appIcon <> NIL) | (control <> NIL) THEN BEGIN
  2459.             HNoPurge(aboutPeek^.appPict);                        {must keep them around}
  2460.              HNoPurge(aboutPeek^.comment);
  2461.             HNoPurge(aboutPeek^.appIcon);
  2462.             MyParamText(StringHandle(aboutPeek^.comment), @userName, @verNum);
  2463.             IF NOT FailLowMemory(0) THEN
  2464.                 theErr:= AsynchSndPlay(Get1Resource('snd ', rMoofSound));
  2465.         END ELSE BEGIN                                                {couldn’t build window}
  2466.             DoErrorSound(1);
  2467.             ignore:= DoCloseWindow(WindowPtr(aboutPeek));
  2468.         END;
  2469.     END ELSE
  2470.         DoErrorSound(1);
  2471. END; {DoAbout}
  2472.  
  2473. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2474. {$S Main}
  2475. PROCEDURE PlaySelectedSnd(sndDoc: SndDocPeek; message: INTEGER);
  2476.  
  2477. {Get the selected sound and pass it to AsynchSndPlay or HyperSndPlay.  This
  2478.  depends on the normal flag.  HyperCard’s method is not normal.  This is
  2479.  the HyperCard version of playing a sound.  It resamples any sound to
  2480.  middle C.  If the resource is too large to fit in memory, I’ll get a NIL
  2481.  handle error from the Sound Unit.  It might be nice to test for this
  2482.  before calling the Sound Unit and telling the user they’re too low on
  2483.  memory, but the unit is robust enough and reports the error.  It’s
  2484.  important to call KillSound to dispose of any data that was
  2485.  allocated if an error were to occur.}
  2486.  
  2487. VAR
  2488.     sndHandle:    Handle;
  2489.     theErr:        OSErr;
  2490.  
  2491. BEGIN
  2492.     theErr:= GetSelection(sndDoc, sndHandle);
  2493.     IF theErr = noErr THEN BEGIN
  2494.         IF message = sPlayingMsg THEN
  2495.             theErr:= AsynchSndPlay(sndHandle)
  2496.         ELSE
  2497.             theErr:= HyperSndPlay(sndHandle);
  2498.     END;
  2499.     IF theErr = noErr THEN BEGIN
  2500.         sndDoc^.sndInUse:= TRUE;                    {this document has a snd in use}
  2501.         ShowStatusWindow(message);
  2502.     END ELSE BEGIN
  2503.         KillSound;
  2504.         AlertUser(theErr, sSoundErr);
  2505.     END;
  2506. END; {PlaySelectedSnd}
  2507.  
  2508. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2509. {$S Main}
  2510. PROCEDURE PlaySndSong(sndDoc: SndDocPeek; sndID: INTEGER);
  2511.  
  2512. {Given a sound resource ID, this will get my song resource and call the
  2513.  Sound Unit to use that sampled sound and song to play a tune.  This
  2514.  application will play one of two songs for sampled sounds.  If any error
  2515.  is encountered, I call KillSound to dispose of all data.}
  2516.  
  2517. VAR
  2518.     chan:             SndChannelPtr;
  2519.     sndInst,
  2520.     sndSong:            Handle;
  2521.     theErr:            OSErr;
  2522.  
  2523. BEGIN
  2524.     theErr:= GetSelection(sndDoc, sndInst);
  2525.     IF theErr = noErr THEN BEGIN
  2526.         sndSong:= Get1Resource('snd ', sndID);
  2527.         theErr:= ResError;                                {save any error}
  2528.         IF sndSong <> NIL THEN BEGIN
  2529.             theErr:= GetSampleChan(chan, kInitNone, sndInst);
  2530.             IF theErr = noErr THEN BEGIN
  2531.                 sndDoc^.sndInUse:= TRUE;                {this document has a snd in use}
  2532.                 theErr:= PlaySong(chan, sndSong);
  2533.             END;
  2534.         END;
  2535.     END;
  2536.     IF theErr = noErr THEN BEGIN
  2537.         IF sndID = rScaleSnd THEN
  2538.             ShowStatusWindow(sScaleMsg)
  2539.         ELSE                                                    {I can play scales or a melody}
  2540.             ShowStatusWindow(sMelodyMsg);
  2541.     END ELSE BEGIN                                            {catch any errors}
  2542.         KillSound;
  2543.         AlertUser(theErr, sSoundErr);
  2544.     END;
  2545. END; {PlaySndSong}
  2546.  
  2547. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2548. {$S Main}
  2549. PROCEDURE PlaySquareSong(sndID: INTEGER);
  2550.  
  2551. {Given a sound resource ID, this will get my song resource and call the
  2552.  Sound Unit to use the squareWaveSynth and song to play a tune.  I request
  2553.  the squareWaveSynth’s timbre (sounds like “tom burr”).  If any error is
  2554.  encountered, I call KillSound to dispose of all data.}
  2555.  
  2556. VAR
  2557.     theErr:            OSErr;
  2558.     sndSong:            Handle;
  2559.     chan:             SndChannelPtr;
  2560.  
  2561. BEGIN
  2562.     sndSong:= Get1Resource('snd ', sndID);
  2563.     IF (sndSong <> NIL) THEN BEGIN
  2564.         theErr:= GetSquareChan(chan, kPreferredTimbre);
  2565.         IF theErr = noErr THEN BEGIN
  2566.             theErr:= PlaySong(chan, sndSong);
  2567.             IF theErr = noErr THEN BEGIN
  2568.                 IF sndID = rScaleSnd THEN
  2569.                     ShowStatusWindow(sScaleMsg)
  2570.                 ELSE                                        {I play scales or a melody}
  2571.                     ShowStatusWindow(sMelodyMsg);
  2572.             END;
  2573.         END;
  2574.         IF theErr <> noErr THEN BEGIN                {catch any errors}
  2575.             KillSound;
  2576.             AlertUser(theErr, sSoundErr);
  2577.         END;
  2578.     END ELSE
  2579.         AlertUser(ResError, sResErr);            {I’ll return the resource error}
  2580. END; {PlaySquareSong}
  2581.  
  2582. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2583. {$S Main}
  2584. PROCEDURE PlaySquareTimbres;
  2585.  
  2586. {This is a demonstration of the squareWaveSynth’s tone qualities (or the lack
  2587.  there of).  This simply loops through timbres sending alternating frequency and
  2588.  rest commands.  Once all the sound commands have been sent, then I need to
  2589.  send a callBackCmd to signal the SoundUnit to dispose of the channel.  Well,
  2590.  actually the SoundUnit will set a global flag that the application will be
  2591.  polling for later in the event loop.  Once this happens, or if any errors
  2592.  are encountered along the way, KillSound will be called.  One very
  2593.  disappointing aspect to changing the timbre is that the Mac Plus or SE
  2594.  cannot handle this while a sound is heard.  The Apple Sound Chip can and
  2595.  if you wanted to remove the rests try this routine on a Mac II, for
  2596.  example, you can hear a continuous sound while the timbre changes.  Try
  2597.  this on a Mac Plus and you’ll hear garbage.  I’d show this myself, but how
  2598.  do I determine if the Mac has the ASC?
  2599.  
  2600.  BUG NOTE: There is problem when the final sound command is a freqDurationCmd.
  2601.  The frequency will continue to sound, looping forever, until a quietCmd is sent
  2602.  or the channel is disposed of.  To prevent unwanted looping, I send a
  2603.  quietCmd after all frequencies.}
  2604.  
  2605. VAR
  2606.     chan:             SndChannelPtr;
  2607.     timbre:            INTEGER;
  2608.     theErr:            OSErr;
  2609.  
  2610. BEGIN
  2611.     theErr:= GetSquareChan(chan, kPreferredTimbre);
  2612.     IF theErr = noErr THEN BEGIN
  2613.         ShowStatusWindow(sTimbresMsg);
  2614.         FOR timbre:= kSineWave TO kSquareWave DO BEGIN
  2615.             theErr:= SetSquareTimbre(chan, timbre, kWait);
  2616.             IF theErr = noErr THEN
  2617.                 theErr:= SendFreqDur(chan, kOneSecond DIV 2, kOctave7 + Akey);
  2618.             IF theErr = noErr THEN
  2619.                 theErr:= SendRest(chan, kOneSecond DIV 10);
  2620.             timbre:= timbre + 8;                            {skip a few more timbres}
  2621.             IF theErr <> noErr THEN                        {if there was an error...}
  2622.                 timbre:= kSquareWave + 1;                {get out of the loop}
  2623.         END;
  2624.     END;
  2625.     IF theErr = noErr THEN
  2626.         theErr:= SoundComplete(chan)
  2627.     ELSE BEGIN                                                {catch any errors}
  2628.         KillSound;
  2629.         AlertUser(theErr, sSoundErr);
  2630.     END;
  2631. END; {PlaySquareTimbres}
  2632.  
  2633. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2634. {$S Main}
  2635. PROCEDURE PlayWaveScale;
  2636.  
  2637. {This demonstrates the wave table synthesizers.  First thing to do is
  2638.  obtain a wave cycle and the 'snd ' resource containing a song to be
  2639.  played.  Then I’m ready to get the four wave channels and install the
  2640.  chosen wave.  Once the wave is installed into the channel I can dispose of
  2641.  the memory used to create the wave, since the Sound Manager will copy it
  2642.  to its internal buffers.  At this point I’m ready to play the song.  It’s
  2643.  not easy to hear four wave synths playing the same frequencies with the
  2644.  same wave table.  I was going to add a modifier to the channel that would
  2645.  transpose each channels frequency commands, but I found a bug.
  2646.  
  2647.  BUG NOTE: Installing a modifier to one of the wave table channels caused
  2648.  the channel to fail.  My normal sequence of events is this: I synchronize
  2649.  the four channels, send all the note commands, end this with the
  2650.  callBackCmd, and finally release the channels to play their queues.  When
  2651.  I added a modifier, the callBackCmd was the first command to be processed.
  2652.  This caused my completion routines to be called and before the channel
  2653.  made any sound it was disposed.  I tried this without the callBackCmd and
  2654.  the channel never processed any commands.
  2655.  
  2656.  BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound
  2657.  chip based Mac.  At least not so far with any System 6.0x releases.  This
  2658.  leaves the Mac Plus/SE without four tone polyphonic sound.}
  2659.  
  2660. VAR
  2661.     chan1, chan2,
  2662.     chan3, chan4:        SndChannelPtr;
  2663.     sndSong,
  2664.     waveHandle:            Handle;
  2665.     waveTablePtr:        Ptr;
  2666.     offSet:                LONGINT;
  2667.     sndType,
  2668.     waveLength:            INTEGER;
  2669.     theErr:                OSErr;
  2670.  
  2671. BEGIN
  2672.     IF (gMac.machineType IN [env512KE, envMacPlus, envSE])
  2673.      & (NOT HasNewSndMgr) THEN BEGIN
  2674.         AlertUser(badChannel, sWavesBroken);        {waves not available}
  2675.         EXIT(PlayWaveScale);                                {we’re out of here}
  2676.     END;
  2677.     sndSong:= Get1Resource('snd ', rScaleSnd);
  2678.     IF sndSong <> NIL THEN BEGIN
  2679.         waveHandle:= Get1Resource('snd ', rTenorVox);
  2680.         theErr:= HoldSnd(waveHandle);
  2681.         IF theErr = noErr THEN BEGIN
  2682.             offSet:= GetSndDataOffset(waveHandle, sndType, waveLength);
  2683.             waveTablePtr:= Ptr(ORD4(waveHandle^) + offSet);
  2684.             theErr:= GetWaveChans(chan1, chan2, chan3, chan4);
  2685.             IF theErr = noErr THEN BEGIN
  2686.                 theErr:= InstallWave(chan1, waveTablePtr, waveLength);
  2687.                 IF theErr = noErr THEN BEGIN
  2688.                     theErr:= InstallWave(chan2, waveTablePtr, waveLength);
  2689.                     IF theErr = noErr THEN BEGIN
  2690.                         theErr:= InstallWave(chan3, waveTablePtr, waveLength);
  2691.                         IF theErr = noErr THEN
  2692.                             theErr:= InstallWave(chan4, waveTablePtr, waveLength);
  2693.                     END;
  2694.                 END;
  2695.             END;
  2696.             HUnlock(waveHandle);
  2697.             HPurge(waveHandle);
  2698.             IF theErr = noErr THEN
  2699.                 theErr:= Play4Waves(chan1, chan2, chan3, chan4,
  2700.                                           sndSong, sndSong, sndSong, sndSong);
  2701.             IF theErr = noErr THEN
  2702.                 ShowStatusWindow(sScaleMsg)
  2703.             ELSE BEGIN                                {catch any sound unit errors}
  2704.                 KillSound;
  2705.                 AlertUser(theErr, sSoundErr);
  2706.             END;
  2707.         END ELSE
  2708.             AlertUser(theErr, sResErr);        {couldn’t get waveHandle}
  2709.     END ELSE
  2710.         AlertUser(ResError, sResErr);            {couldn’t get song}
  2711. END;
  2712.  
  2713. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2714. {$S Main}
  2715. FUNCTION GetSndSongs(sndSongID1, sndSongID2, sndSongID3, sndSongID4: INTEGER;
  2716.                             VAR sndSong1, sndSong2, sndSong3, sndSong4: Handle): OSErr;
  2717.  
  2718. {This is a utility routine used to obtain the four snd resources that
  2719.  contain parts of a song.  Nothing much to do other then get the resource
  2720.  and do error checking.}
  2721.  
  2722. BEGIN
  2723.     GetSndSongs:= noErr;                                        {initialize result}
  2724.     sndSong1:= Get1Resource('snd ', sndSongID1);     {get all the snds}
  2725.     IF sndSong1 <> NIL THEN BEGIN
  2726.         sndSong2:= Get1Resource('snd ', sndSongID2);
  2727.         IF sndSong2 <> NIL THEN BEGIN
  2728.             sndSong3:= Get1Resource('snd ', sndSongID3);
  2729.             IF sndSong3 <> NIL THEN
  2730.                 sndSong4:= Get1Resource('snd ', sndSongID4);
  2731.         END;
  2732.     END;
  2733.     IF (sndSong1 = NIL) | (sndSong2 = NIL)
  2734.      | (sndSong3 = NIL) | (sndSong4 = NIL) THEN
  2735.         GetSndSongs:= ResError;                                {return resource error}
  2736. END;
  2737.  
  2738. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2739. {$S Main}
  2740. FUNCTION InstallWaveSnds(chan1, chan2, chan3, chan4: SndChannelPtr;
  2741.                                     waveID1, waveID2, waveID3, waveID4: INTEGER): OSErr;
  2742.  
  2743. {This routine is a utility I’m using to prepare four wave table channels.
  2744.  First thing to do is obtain a wave table data I stored in a set of 'snd '
  2745.  resources.  Then I’m ready to install these wave tables into the four wave
  2746.  channels.  Once the waves are installed into the channel I can dispose of
  2747.  the memory used to create each wave, since the Sound Manager will copy
  2748.  them to its internal buffers.  At this point the channels are ready to
  2749.  play sounds.}
  2750.  
  2751. VAR
  2752.     waveSnd1, waveSnd2,
  2753.     waveSnd3, waveSnd4:    Handle;
  2754.     wavePtr:                    Ptr;
  2755.     offSet:                    LONGINT;
  2756.     waveLgth, sndType:    INTEGER;
  2757.     theErr:                    OSErr;
  2758.  
  2759. BEGIN
  2760.     waveSnd1:= Get1Resource('snd ', waveID1);        {get em and hold em down}
  2761.     theErr:= HoldSnd(waveSnd1);
  2762.     IF theErr = noErr THEN BEGIN
  2763.         waveSnd2:= Get1Resource('snd ', waveID2);
  2764.         theErr:= HoldSnd(waveSnd2);
  2765.         IF theErr = noErr THEN BEGIN
  2766.             waveSnd3:= Get1Resource('snd ', waveID3);
  2767.             theErr:= HoldSnd(waveSnd3);
  2768.             IF theErr = noErr THEN BEGIN
  2769.                 waveSnd4:= Get1Resource('snd ', waveID4);
  2770.                 theErr:= HoldSnd(waveSnd4);
  2771.             END;
  2772.         END;
  2773.     END;
  2774.     InstallWaveSnds:= theErr;                            {return the error}
  2775.     IF theErr <> noErr THEN
  2776.         EXIT(InstallWaveSnds);                            {we’re out of here}
  2777.                                                                 {catch the waves}
  2778.     offSet:= GetSndDataOffset(waveSnd1, sndType, waveLgth);
  2779.     wavePtr:= Ptr(ORD4(waveSnd1^) + offSet);
  2780.     theErr:= InstallWave(chan1, wavePtr, waveLgth);
  2781.     IF theErr = noErr THEN BEGIN
  2782.         offSet:= GetSndDataOffset(waveSnd2, sndType, waveLgth);
  2783.         wavePtr:= Ptr(ORD4(waveSnd2^) + offSet);
  2784.         theErr:= InstallWave(chan2, wavePtr, waveLgth);
  2785.         IF theErr = noErr THEN BEGIN
  2786.             offSet:= GetSndDataOffset(waveSnd3, sndType, waveLgth);
  2787.             wavePtr:= Ptr(ORD4(waveSnd3^) + offSet);
  2788.             theErr:= InstallWave(chan3, wavePtr, waveLgth);
  2789.             IF theErr = noErr THEN BEGIN
  2790.                 offSet:= GetSndDataOffset(waveSnd4, sndType, waveLgth);
  2791.                 wavePtr:= Ptr(ORD4(waveSnd4^) + offSet);
  2792.                 theErr:= InstallWave(chan4, wavePtr, waveLgth);
  2793.             END;
  2794.         END;
  2795.     END;
  2796.     HUnlock(waveSnd1);                                    {done with resources}
  2797.     HPurge(waveSnd1);
  2798.     HUnlock(waveSnd2);
  2799.     HPurge(waveSnd2);
  2800.     HUnlock(waveSnd3);
  2801.     HPurge(waveSnd3);
  2802.     HUnlock(waveSnd4);
  2803.     HPurge(waveSnd4);
  2804.     InstallWaveSnds:= theErr;                            {return the error}
  2805. END;
  2806.  
  2807. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2808. {$S Main}
  2809. PROCEDURE PlayWaveMelody;
  2810.  
  2811. {This is the demonstration of the wave table synthesizers.  This will get
  2812.  four snd resources that contain parts to a song.  Then I get the four wave
  2813.  table channels.  With these four channels, I install two other snd
  2814.  resources that contain wave table data.  Once the four songs, four
  2815.  channels, and two wave tables are ready then I play the song.
  2816.  
  2817.  BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound
  2818.  chip based Mac.  At least not so far with any System 6.0x release.  This
  2819.  leaves the Mac Plus/SE without four tone polyphonic sound.}
  2820.  
  2821. VAR
  2822.     sndSong1, sndSong2,
  2823.     sndSong3, sndSong4:    Handle;
  2824.     chan1, chan2,
  2825.     chan3, chan4:            SndChannelPtr;
  2826.     theErr:                    OSErr;
  2827.  
  2828. BEGIN
  2829.     theErr:= noErr;
  2830.     IF (gMac.machineType IN [env512KE, envMacPlus, envSE])
  2831.      & (NOT HasNewSndMgr) THEN BEGIN
  2832.         AlertUser(badChannel, sWavesBroken);            {waves not available}
  2833.         EXIT(PlayWaveMelody);                                {we’re out of here}
  2834.     END;
  2835.     theErr:= GetSndSongs(rMelodyPart1, rMelodyPart2, rMelodyPart3, rMelodyPart4,
  2836.                                 sndSong1, sndSong2, sndSong3, sndSong4);
  2837.     IF theErr <> noErr THEN BEGIN
  2838.         AlertUser(theErr, sResErr);                {return the error}
  2839.         EXIT(PlayWaveMelody);
  2840.     END;
  2841.     theErr:= GetWaveChans(chan1, chan2, chan3, chan4);
  2842.     IF theErr = noErr THEN BEGIN
  2843.         theErr:= InstallWaveSnds(chan1, chan2, chan3, chan4,
  2844.                                     rWaveMelody, rWaveHarmony, rWaveHarmony, rWaveHarmony);
  2845.         IF theErr = noErr THEN
  2846.             theErr:= Play4Waves(chan1, chan2, chan3, chan4,
  2847.                                           sndSong1, sndSong2, sndSong3, sndSong4);
  2848.     END;
  2849.     IF theErr = noErr THEN
  2850.         ShowStatusWindow(sMelodyMsg)
  2851.     ELSE BEGIN                                        {catch any errors}
  2852.         KillSound;
  2853.         AlertUser(theErr, sSoundErr);
  2854.     END;
  2855. END;
  2856.  
  2857. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2858. {$S Main}
  2859. PROCEDURE PlayWaveSATB;
  2860.  
  2861. {This is the demonstration of the wave table synthesizers.  This will get
  2862.  four snd resources that contain parts to a song.  Then I get the four wave
  2863.  table channels.  With these four channels, I install four other snd resources
  2864.  that contain wave table data.  Once the four songs, four channels, and
  2865.  four wave tables are ready then I play the song.  By the way, SATB stands
  2866.  for Soprano, Alto, Tenor, and Bass.  It’s standard music-speak.
  2867.  
  2868.  BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound
  2869.  chip based Mac.  At least not so far with any System 6.0x release.  This
  2870.  leaves the Mac Plus/SE without four tone polyphonic sound.}
  2871.  
  2872. VAR
  2873.     sndSong1, sndSong2,
  2874.     sndSong3, sndSong4:    Handle;
  2875.     chan1, chan2,
  2876.     chan3, chan4:            SndChannelPtr;
  2877.     theErr:                    OSErr;
  2878.  
  2879. BEGIN
  2880.     theErr:= noErr;
  2881.     IF (gMac.machineType IN [env512KE, envMacPlus, envSE])
  2882.      & (NOT HasNewSndMgr) THEN BEGIN
  2883.         AlertUser(badChannel, sWavesBroken);            {waves not available}
  2884.         EXIT(PlayWaveSATB);                                    {we’re out of here}
  2885.     END;
  2886.     theErr:= GetSndSongs(rCounterPt1, rCounterPt2, rCounterPt3, rCounterPt4,
  2887.                                 sndSong1, sndSong2, sndSong3, sndSong4);
  2888.     IF theErr <> noErr THEN BEGIN
  2889.         AlertUser(theErr, sResErr);                        {return the error}
  2890.         EXIT(PlayWaveSATB);
  2891.     END;
  2892.     theErr:= GetWaveChans(chan1, chan2, chan3, chan4);
  2893.     IF theErr = noErr THEN BEGIN
  2894.         theErr:= InstallWaveSnds(chan1, chan2, chan3, chan4,
  2895.                                             rSopranoVox, rAltoVox, rTenorVox, rBassVox);
  2896.         IF theErr = noErr THEN
  2897.             theErr:= Play4Waves(chan1, chan2, chan3, chan4,
  2898.                                           sndSong1, sndSong2, sndSong3, sndSong4);
  2899.     END;
  2900.     IF theErr = noErr THEN
  2901.         ShowStatusWindow(sCounterPtMsg)
  2902.     ELSE BEGIN                                                    {catch any errors}
  2903.         KillSound;
  2904.         AlertUser(theErr, sSoundErr);
  2905.     END;
  2906. END;
  2907.  
  2908.  
  2909. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2910. {$S Main}
  2911. PROCEDURE DefaultOutline(window: WindowPtr; theItem: INTEGER);
  2912.  
  2913. {VERSION 1.1:  This is just one of the reasons I hate the Dialog Manager.  
  2914.  I needed a simple dialog to ask the user for a name for their new sound.  
  2915.  To do this, I only needed a simple dialog with two buttons and an 
  2916.  editable text item.  Since the ModalDialog will not draw an outline 
  2917.  around the default item, I then needed an userItem.  This is the really 
  2918.  stupid part. The userItem is only there just to get a chance to draw an 
  2919.  outline around the default button.  It has no other purpose.  It is not 
  2920.  visible, not does it get any user interaction what so ever.  It's just a 
  2921.  pain in the ass.}
  2922.  
  2923. VAR
  2924.     itemHndl:            Handle;
  2925.     itemRect:            Rect;
  2926.     kind:                    INTEGER;
  2927.  
  2928. BEGIN
  2929.     GetDialogItem(DialogPtr(window), ok, kind, itemHndl, itemRect);
  2930.     DoButtonOutline(ControlHandle(itemHndl));
  2931. END;
  2932.  
  2933. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2934. {$S Main}
  2935. PROCEDURE GetSndName(VAR sndName: Str255);
  2936.  
  2937. {VERSION 1.1:  Show a dialog asking the user to name their new sound 
  2938.  resource.  I need to outline the default button, so I have to install an 
  2939.  userItem setting its drawing procedure to do the outline around a 
  2940.  different item altogether.  The Dialog Manager really bugs me.}
  2941.  
  2942. VAR
  2943.     dialog:                DialogPtr;
  2944.     itemHndl:            Handle;
  2945.     itemRect:            Rect;
  2946.     kind:                    INTEGER;
  2947.     theItem:                INTEGER;
  2948.  
  2949. BEGIN
  2950.     dialog:= GetCenteredDialog(rGetNameDLOG, NIL, WindowPtr(-1));
  2951.     GetDialogItem(dialog, rUserItem, kind, itemHndl, itemRect);
  2952.     SetDialogItem(dialog, rUserItem, kind, @DefaultOutline, itemRect);
  2953.     REPEAT
  2954.         ModalDialog(NIL, theItem);
  2955.     UNTIL (theItem = ok) | (theItem = cancel);
  2956.     IF theItem = ok THEN BEGIN
  2957.         GetDialogItem(dialog, rNameItem, kind, itemHndl, itemRect);
  2958.         GetDialogItemText(itemHndl, sndName);
  2959.     END ELSE
  2960.         sndName:= '';
  2961.     DisposeDialog(dialog);
  2962. END;
  2963.  
  2964. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  2965. {$S Main}
  2966. FUNCTION AddSnd(sndDoc: SndDocPeek; sndNamePtr: StringPtr; sndHndl: Handle): OSErr;
  2967.  
  2968. {VERSION 1.1:  This routine adds the given snd resource to the file and 
  2969.  document. First thing to do is find a proper resource ID for the 'snd '.  
  2970.  There is a reserved range for them, didn't you read the documentation?  
  2971.  Once this new resource is added then I update the file and re-build the 
  2972.  list of sounds.  This is necessary since the list must be in the same 
  2973.  order as the sounds are in the file.  I ignore the error returned by 
  2974.  InitSndList, since the chances of it failing at this point are slim.  If 
  2975.  anything is wrong with the list, then the rest of this application is 
  2976.  robust enough to handle a resource problem.  The user would have to close 
  2977.  the document and attempt to open it again.  If adding the resources 
  2978.  works, but updating the file fails then I will remove it.  This keeps the 
  2979.  document consistent with the file.}
  2980.  
  2981. VAR
  2982.     resID:            INTEGER;
  2983.     theErr:            OSErr;
  2984.     ignore:                INTEGER;
  2985.  
  2986. BEGIN
  2987.     UseResFile(sndDoc^.resFile);                {put our resource in the right file}
  2988.     REPEAT
  2989.         resID:= Unique1ID('snd ');
  2990.     UNTIL resID > kSystemSndRange;
  2991.     AddResource(sndHndl, 'snd ', resID, sndNamePtr^);
  2992.     theErr:= ResError;
  2993.     UseResFile(gAppResRef);                        {restore our resource file}
  2994.     IF theErr = noErr THEN BEGIN
  2995.         UpdateResFile(sndDoc^.resFile);        {update the file}
  2996.         theErr:= ResError;
  2997.         IF theErr = noErr THEN BEGIN
  2998.             ignore:= FlushVol(nil, sndDoc^.vRefNum);
  2999.             LDelRow(0, 0, sndDoc^.list);        {delete all the rows}
  3000.             ignore:= InitSndList(sndDoc);        {re-create the list}
  3001.             SelectSndCell(sndDoc, resID);        {select the new snd}
  3002.         END ELSE
  3003.             RemoveResource(sndHndl);                {couldn't add it, update failed}
  3004.     END;
  3005.     AddSnd:= theErr;
  3006. END;
  3007.  
  3008. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3009. {$S Main}
  3010. PROCEDURE ClearSnd(sndDoc: SndDocPeek);
  3011.  
  3012. {VERSION 1.1:  This routine has one tricky aspect involving the Resource 
  3013.  Manager and removing a resource.  There's a catch.  I want the document 
  3014.  to always reflect the contents of the file on disk.  In other words, 
  3015.  InitSndList depends on UpdateResFile returning noErr.  If the file is 
  3016.  locked, then removing a resource shouldn't be allowed but RemoveResource 
  3017.  will return noErr even if the file cannot be updated.  This is a 
  3018.  "feature" since the user may unlock the volume after removing the 
  3019.  resource and then UpdateResFile would work.  If RemoveResource succeeds, 
  3020.  but UpdateResFile fails then the resource map in memory will be 
  3021.  inconsistent with what's on disk.  If the user then closed the document 
  3022.  and opened it again, the resource they just removed would magically still 
  3023.  be there.  The basic problem is that it is difficult to determine if a 
  3024.  given file will really allow write access.  There's at least three 
  3025.  situations to check: a locked file, a locked volume, or an AppleShare 
  3026.  folder privileges issue.  The ideal, and probably the best, solution 
  3027.  would be to deselect the edit commands from the user for a read-only 
  3028.  file.  This would involve lots of File Manager calls every time the user 
  3029.  selects a menu command.  (If you thought that GetFCBInfo would tell you 
  3030.  this, you're wrong.  You can have write permission to a file that is on a 
  3031.  locked volume.)  Additionally, the user needs to see that the reason the 
  3032.  edit menu doesn't work is because the file is read-only which would 
  3033.  require another feature to be added with lots more code.  This is why I 
  3034.  call ChangedResource before any other Resource Manager calls.  
  3035.  ChangedResource will determine if the resource can be written to disk.  
  3036.  If not, then it returns an error which is exactly what I wanted to know 
  3037.  in the first place.  I want to flush the cache to keep the disk's 
  3038.  resource map consistent with the resource data.  Otherwise, a crash could 
  3039.  occur the resource fork might be damaged and the file has to be repaired 
  3040.  (if possible) or deleted.  I'm ignoring the InitSndList result.  At this 
  3041.  stage, there's only a slim chance the list couldn't be re-built.  If it 
  3042.  does fail, the file should be closed.
  3043.  
  3044.  A tip to the reader: dispose of as much memory as possible before calling 
  3045.  UpdateResFile.  This makes it faster.  UpdateResFile will not purge any 
  3046.  memory, but only attempts to use the available space.  If there's little 
  3047.  free space then UpdateResFile will run really slow.}
  3048.  
  3049. VAR
  3050.     resFName:            Str255;
  3051.     sndHndl:                Handle;
  3052.     theErr, openErr:    OSErr;
  3053.     ignore:                INTEGER;
  3054.     closed:                BOOLEAN;
  3055.     
  3056. BEGIN
  3057.     theErr:= GetSelection(sndDoc, sndHndl);        {get the resource to remove}
  3058.     IF theErr = noErr THEN BEGIN
  3059.         ChangedResource(sndHndl);                        {can we change the file?}
  3060.         theErr:= ResError;                                {save the error result}
  3061.         IF theErr = noErr THEN BEGIN
  3062.             UseResFile(sndDoc^.resFile);                {use the right resource file}
  3063.             RemoveResource(sndHndl);                        {remove that sucker}
  3064.             theErr:= ResError;                            {save the error result}
  3065.             UseResFile(gAppResRef);                        {restore our resource file}
  3066.             IF theErr = noErr THEN BEGIN
  3067.                 DisposeHandle(sndHndl);                    {get rid of the memory}
  3068.                 UpdateResFile(sndDoc^.resFile);        {update the file}
  3069.                 theErr:= ResError;                        {save the error result}
  3070.                 IF theErr = noErr THEN
  3071.                     ignore:= FlushVol(nil, sndDoc^.vRefNum);
  3072.                 LDelRow(0, 0, sndDoc^.list);            {delete all the rows}
  3073.                 ignore:= InitSndList(sndDoc);            {re-create the list}
  3074.                 SelectNextCell(sndDoc^.list, TRUE); {select the first cell}
  3075.                 ActivateSndCntls(sndDoc);                {may not be any sounds left}
  3076.             END;
  3077.         END;
  3078.     END;
  3079.     IF theErr <> noErr THEN
  3080.         AlertUser(theErr, sEditErr);
  3081. END; {ClearSnd}
  3082.  
  3083. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3084. {$S Main}
  3085. PROCEDURE DoRecordSound(sndDoc: SndDocPeek);
  3086.  
  3087. {VERSION 1.1:  This routine will create a handle and record sound into it.  
  3088.  A new sound will prompt the user for a name to add it to the document.  
  3089.  The Sound Input Manager will re-size this handle to be as small as 
  3090.  possible to contain only the samples recorded.  You can pass sndHandl = 
  3091.  NIL to SndRecord, which will cause the Sound Input Manager to create a 
  3092.  handle for you.  If adding the new sound is successful, the new resource 
  3093.  handle is marked purgeable, because I only use them temporally and they 
  3094.  tend to be large.  I have to set the sndHandle variable to NIL in this 
  3095.  case since it is now belongs to the Resource Manager and I don't want to 
  3096.  dispose of it in the error handling code.  The choice of recording 
  3097.  quality wasn't given to the user.  Typically, users would only be 
  3098.  confused by the question of "what compression ratio do you prefer?"  As
  3099.  a power user option, it would be nice to let the user set the rate.  Be
  3100.  careful since this is a user interface issue, and Apple expects to see
  3101.  lots of Sound Input features in applications.}
  3102.  
  3103. VAR
  3104.     sndName:                Str255;
  3105.     total:                LONGINT;
  3106.     contig:                LONGINT;
  3107.     sndHandle:            Handle;
  3108.     theErr:                OSErr;
  3109.  
  3110. BEGIN
  3111.     KillSound;
  3112.     PurgeSpace(total, contig);
  3113.     sndHandle:= NewHandle(contig - kMinSpace);
  3114.     IF sndHandle <> NIL THEN BEGIN
  3115.         theErr:= SndRecord(NIL, Point(kSFTopLeft), siBestQuality, SndListHandle(sndHandle));
  3116.         IF theErr = noErr THEN BEGIN
  3117.             GetSndName(sndName);
  3118.             theErr:= AddSnd(sndDoc, @sndName, sndHandle);
  3119.             IF theErr = noErr THEN BEGIN
  3120.                 HPurge(sndHandle);
  3121.                 sndHandle:= NIL;
  3122.             END;
  3123.         END;
  3124.     END ELSE
  3125.         theErr:= MemError;
  3126.     IF sndHandle <> NIL THEN
  3127.         DisposeHandle(sndHandle);
  3128.     IF (theErr <> noErr) & (theErr <> userCanceledErr) THEN
  3129.         AlertUser(theErr, sSoundErr);
  3130. END;
  3131.  
  3132. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3133. {$S Main}
  3134. PROCEDURE AdjustMenus;
  3135.  
  3136. {Enable and disable menus based on the current state.  The user can only
  3137.  select enabled menu items so I set up all the menu items before calling
  3138.  MenuSelect or MenuKey, since these are the only times that a menu item
  3139.  can be selected. Note that MenuSelect is also the only time the user will
  3140.  see menu items. This approach to deciding what enable/disable state a menu
  3141.  item has the advantage of concentrating all the decision making in one
  3142.  routine, as opposed to being spread throughout the application.  Other
  3143.  application designs may take a different approach that may or may not be
  3144.  just as valid.}
  3145.  
  3146. VAR
  3147.      menu:            MenuHandle;
  3148.     window:        WindowPtr;
  3149.     allowEdit:    BOOLEAN;
  3150.  
  3151. BEGIN
  3152.     window:= FrontWindow;
  3153.  
  3154.     menu:= GetMenuHandle(mFile);                        {the File menu and items}
  3155.     IF IsDAWindow(window) | IsDocWindow(window) THEN
  3156.         EnableItem(menu, iClose)
  3157.     ELSE
  3158.         DisableItem(menu, iClose);
  3159.  
  3160.     menu:= GetMenuHandle(mEdit);                        {the Edit menu and items}
  3161.     allowEdit:= IsDAWindow(window);
  3162.     IF allowEdit THEN                                    {check the undo item}
  3163.         EnableItem(menu, iUndo)
  3164.     ELSE
  3165.         DisableItem(menu, iUndo);
  3166.  
  3167.     IF IsDocWindow(window) THEN BEGIN            {handle the other edit items}
  3168.         allowEdit:= HasSelection(SndDocPeek(window)) | allowEdit;
  3169.         EnableItem(menu, iPaste);
  3170.     END ELSE
  3171.         DisableItem(menu, iPaste);
  3172.         
  3173.     IF allowEdit THEN BEGIN
  3174.         EnableItem(menu, iCut);
  3175.         EnableItem(menu, iCopy);
  3176.         EnableItem(menu, iClear);
  3177.     END ELSE BEGIN
  3178.         DisableItem(menu, iCut);
  3179.         DisableItem(menu, iCopy);
  3180.         DisableItem(menu, iClear);
  3181.     END;
  3182. END; {AdjustMenus}
  3183.  
  3184. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3185. {$S Main}
  3186. PROCEDURE CopySnd(sndDoc: SndDocPeek);
  3187.  
  3188. {VERSION 1.1:  Get the currently selected item and copy it to the 
  3189.  clipboard.  I want to also save the name for this resource, so I add a 
  3190.  string type to the clipboard as well.}
  3191.  
  3192. VAR
  3193.     sndName:            Str255;
  3194.     rType:            ResType;
  3195.     scrapLen:        LONGINT;
  3196.     id:                INTEGER;
  3197.     sndHndl:            Handle;
  3198.     theErr:            OSErr;
  3199.     
  3200. BEGIN
  3201.     theErr:= GetSelection(sndDoc, sndHndl);
  3202.     IF theErr = noErr THEN BEGIN
  3203.         GetResInfo(sndHndl, id, rType, sndName);
  3204.         scrapLen:= ZeroScrap;                        {ignoring the result}
  3205.         theErr:= PutScrap(LENGTH(sndName) + 1, 'STR ', @sndName);
  3206.         HLock(sndHndl);
  3207.         theErr:= PutScrap(GetHandleSize(sndHndl), 'snd ', sndHndl^);
  3208.         HUnlock(sndHndl);
  3209.         HPurge(sndHndl);
  3210.     END;
  3211.     IF theErr <> noErr THEN
  3212.         AlertUser(theErr, sEditErr);
  3213. END;
  3214.  
  3215. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3216. {$S Main}
  3217. PROCEDURE CutSnd(sndDoc: SndDocPeek);
  3218.  
  3219. {VERSION 1.1:  Copy the selection and then delete it.}
  3220.  
  3221. BEGIN
  3222.     CopySnd(sndDoc);
  3223.     ClearSnd(sndDoc);
  3224. END;
  3225.  
  3226. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3227. {$S Main}
  3228. PROCEDURE PasteSnd(sndDoc: SndDocPeek);
  3229.  
  3230. {VERSION 1.1:  This routine will create a new resource into the file 
  3231.  containing the clipboard's sound data.  A name is attempted to be found 
  3232.  by first checking for the string type in the scrap.  This application 
  3233.  will always put both a string and a sound together on the clipboard.  But 
  3234.  if the user copied a sound from the Sound cdev, ResEdit, or by some other 
  3235.  method then I'll ask the user for a new name.  If adding the new resource 
  3236.  is successful, then I mark the resource as purgeable and set the sndHndl 
  3237.  variable to NIL.  The Resource Manager then owns the handle and I don't 
  3238.  want my error handling to dispose of this new handle.}
  3239.  
  3240. VAR
  3241.     sndName:            Str255;
  3242.     sndNameHndl:    StringHandle;
  3243.     offset:            LONGINT;
  3244.     scrapLen:        LONGINT;
  3245.     sndHndl:            Handle;
  3246.     theErr:            OSErr;
  3247.  
  3248. BEGIN
  3249.     sndHndl:= NewHandle(0);
  3250.     sndNameHndl:= NewString('');
  3251.     theErr:= MemError;
  3252.     scrapLen:= GetScrap(Handle(sndNameHndl), 'STR ', offset);
  3253.     scrapLen:= GetScrap(sndHndl, 'snd ', offset);
  3254.     IF scrapLen > 0 THEN BEGIN
  3255.         IF sndNameHndl <> NIL THEN
  3256.             sndName:= sndNameHndl^^
  3257.         ELSE
  3258.             sndName:= '';
  3259.         IF sndName = '' THEN
  3260.             GetSndName(sndName);
  3261.         theErr:= AddSnd(sndDoc, @sndName , sndHndl);
  3262.         IF theErr = noErr THEN BEGIN
  3263.             HPurge(sndHndl);
  3264.             sndHndl:= NIL;                                        {done with handle}
  3265.             ActivateSndCntls(sndDoc);
  3266.         END;
  3267.     END ELSE
  3268.         theErr:= scrapLen;
  3269.  
  3270.     IF sndHndl <> NIL THEN
  3271.         DisposeHandle(sndHndl);
  3272.     IF sndNameHndl <> NIL THEN
  3273.         DisposeHandle(Handle(sndNameHndl));
  3274.     IF theErr <> noErr THEN
  3275.         AlertUser(theErr, sEditErr);
  3276. END;
  3277.  
  3278. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3279. {$S Main}
  3280. PROCEDURE DoMenuCommand(menuResult: LONGINT);
  3281.  
  3282. {This is called when an item is chosen from the menu bar (after calling
  3283.  MenuSelect or MenuKey). It performs the right operation for each command.
  3284.  It is good to have both the result of MenuSelect and MenuKey go to
  3285.  one routine like this to keep everything organized.
  3286.  
  3287.  VERSION 1.1:  Supporting the edit commands for snd resources.}
  3288.  
  3289. VAR
  3290.     window:                WindowPtr;
  3291.     menuID, menuItem,                        {resource ID and item of the selected menu}
  3292.     daRefNum:            INTEGER;
  3293.     daName:                Str255;
  3294.     ignore:                BOOLEAN;
  3295.  
  3296. BEGIN
  3297.     menuID:= HiWord(menuResult);        {use MPW’s built-ins for efficiency...}
  3298.     menuItem:= LoWord(menuResult);        {to get menu item number and menu number}
  3299.     CASE menuID OF
  3300.  
  3301.         mApple:
  3302.             CASE menuItem OF
  3303.                 iAbout:                        {bring up alert for About}
  3304.                     DoAbout;
  3305.                 OTHERWISE BEGIN            {all non-About items in this menu are DAs}
  3306.                     GetMenuItemText(GetMenuHandle(mApple), menuItem, daName);
  3307.                     daRefNum:= OpenDeskAcc(daName);
  3308.                 END;
  3309.             END;
  3310.  
  3311.         mFile:
  3312.             CASE menuItem OF
  3313.                 iNew:
  3314.                     NewSoundDoc;
  3315.                 iOpen:
  3316.                     GetSoundDoc;
  3317.                 iClose:
  3318.                     ignore:= DoCloseWindow(FrontWindow); {I don’t care if cancelled}
  3319.                 iQuit:
  3320.                     Terminate;
  3321.             END;
  3322.  
  3323.         mEdit: BEGIN                        {call SystemEdit for DA editing & MultiFinder}
  3324.             IF NOT SystemEdit(menuItem - 1) THEN BEGIN {since I don’t do any editing}
  3325.                 window:= FrontWindow;
  3326.                 IF IsDocWindow(window) THEN BEGIN
  3327.                     CASE menuItem OF
  3328.                         
  3329.                         iCut:
  3330.                             CutSnd(SndDocPeek(window));
  3331.                             
  3332.                         iCopy:
  3333.                             CopySnd(SndDocPeek(window));
  3334.                                 
  3335.                         iPaste:
  3336.                             PasteSnd(SndDocPeek(window));
  3337.                             
  3338.                         iClear:
  3339.                             ClearSnd(SndDocPeek(window));
  3340.  
  3341.                     END;
  3342.                 END;
  3343.             END;
  3344.         END;
  3345.  
  3346.         mDemos:
  3347.             CASE menuItem OF
  3348.  
  3349.                 iSquareScale:
  3350.                     PlaySquareSong(rScaleSnd);
  3351.  
  3352.                 iSquareMelody:
  3353.                     PlaySquareSong(rMelodyPart1);
  3354.  
  3355.                 iSquareTimbre:
  3356.                     PlaySquareTimbres;
  3357.  
  3358.                 iWaveScale:
  3359.                     PlayWaveScale;
  3360.  
  3361.                 iWaveMelody:
  3362.                     PlayWaveMelody;
  3363.  
  3364.                 iWaveSATB:
  3365.                     PlayWaveSATB;
  3366.  
  3367.             END; {CASE menuItem OF}
  3368.  
  3369.     END; {CASE menuID OF}
  3370.     HiliteMenu(0);                        {unhighlight what MenuSelect or MenuKey hilited}
  3371. END; {DoMenuCommand}
  3372.  
  3373. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3374. {$S Main}
  3375. PROCEDURE DrawSndWindow(window: WindowPtr);
  3376.  
  3377. {Draw the contents of the application window in response to an update
  3378.  event.  At this point, BeginUpdate has been called which sets the window’s
  3379.  visRgn to clip drawing only where it needs to be done.  I have the
  3380.  controls to draw, a list to update, and a default button to outline.  I
  3381.  use UpdateControls to avoid needless drawing that happens with DrawControls.
  3382.  It not only runs faster but doesn’t flicker.}
  3383.  
  3384.  VAR
  3385.     control:            ControlHandle;
  3386.     theRect:         Rect;
  3387.  
  3388. BEGIN
  3389.     PenNormal;
  3390.     LUpdate(window^.VisRgn, SndDocPeek(window)^.list);        {update list}
  3391.     theRect:= SndDocPeek(window)^.list^^.rView;                {frame the list}
  3392.     theRect.right:= theRect.right + kScrollBarAdjust;
  3393.     InsetRect(theRect, kListFrameInset, kListFrameInset);
  3394.     FrameRect(theRect);
  3395.     UpdateControls(window, window^.visRgn);                        {update controls}
  3396.     control:= WindowPeek(window)^.controlList;                {draw button outline}
  3397.     WHILE control <> NIL DO BEGIN
  3398.         IF GetControlReference(control) = rPlaySndCntl THEN
  3399.             DoButtonOutline(control);
  3400.         control:= control^^.nextControl;
  3401.     END;
  3402. END; {DrawWindow}
  3403.  
  3404. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3405. {$S Main}
  3406. PROCEDURE DoKeyDown(key: CHAR; window: WindowPtr);
  3407.  
  3408. {When the user types a key, I check if it is one that I’m looking for.
  3409.  This routine only handles the non-command key events.  Here I’m looking
  3410.  for a arrow key or the return and enter keys for the default button.
  3411.  
  3412.  VERSION 1.1: The enter and return keys only work if there is a selection.
  3413.  Now supporting the backspace and delete keys which clear the selection.}
  3414.  
  3415. VAR
  3416.     control:            ControlHandle;
  3417.     ignore:            BOOLEAN;
  3418.     kind:                INTEGER;
  3419.     r:                    Rect;
  3420.  
  3421. BEGIN
  3422.     CASE GetWRefCon(window) OF
  3423.  
  3424.         rSoundWindow: BEGIN
  3425.             CASE key OF
  3426.  
  3427.                 kEnterKey, kReturnKey: BEGIN
  3428.                     IF HasSelection(SndDocPeek(window)) THEN BEGIN
  3429.                         control:= WindowPeek(window)^.controlList;
  3430.                         WHILE control <> NIL DO BEGIN            {find the default button}
  3431.                             IF GetControlReference(control) = rPlaySndCntl THEN
  3432.                                 SelectButton(control);            {here it is}
  3433.                             control:= control^^.nextControl;
  3434.                         END;
  3435.                         PlaySelectedSnd(SndDocPeek(window), sPlayingMsg);
  3436.                     END;
  3437.                 END;
  3438.  
  3439.                 kUpArrow: BEGIN
  3440.                     SelectNextCell(SndDocPeek(window)^.list, FALSE);
  3441.                     ActivateSndCntls(SndDocPeek(window));
  3442.                 END;
  3443.  
  3444.                 kDownArrow: BEGIN
  3445.                     SelectNextCell(SndDocPeek(window)^.list, TRUE);
  3446.                     ActivateSndCntls(SndDocPeek(window));
  3447.                 END;
  3448.                 
  3449.                 kBackspace, kDelete:
  3450.                     ClearSnd(SndDocPeek(window));
  3451.             END;
  3452.         END; {rSoundWindow}
  3453.  
  3454.         rStatusWindow:
  3455.             IF key IN [kEnterKey, kReturnKey, kEscape] THEN BEGIN
  3456.                 SelectButton(WindowPeek(window)^.controlList);
  3457.                 KillSound;
  3458.             END;
  3459.  
  3460.         rAboutWindow:
  3461.             IF key IN [kEnterKey, kReturnKey, kEscape] THEN BEGIN
  3462.                 SelectButton(WindowPeek(window)^.controlList);
  3463.                 ignore:= DoCloseWindow(window);
  3464.             END;
  3465.         END; {CASE GetWRefCon(window)}
  3466. END; {DoKeyDown}
  3467.  
  3468. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3469. {$S Main}
  3470. FUNCTION ListClick(sndDoc: SndDocPeek; event: EventRecord): BOOLEAN;
  3471.  
  3472. {Given a document, this will handle any clicking in the list.  The mouse
  3473.  point (event.where) is expected to have been adjusted to the local
  3474.  coordinates of the window.  If this routine does handle the event, then
  3475.  I return TRUE.
  3476.  
  3477.  BUG NOTE: LClick will return a double-click even if no cell was selected.
  3478.  So I have to test for the last click being in a real cell.  If not, then
  3479.  I will not process the double click.  Also, there is another bug in the
  3480.  List Manager.  It will not always deselect the currently selected cell when
  3481.  the user clicks outside of the data bounds.  In other words, sometimes
  3482.  my list would show 4 items when the list has room to show 8.  If the user
  3483.  clicked in the bottom area of the list (below the last item) the List
  3484.  Manager should deselect any items.  It doesn’t all the time, just sometimes.
  3485.  The only real solution would be to write a new LClick.}
  3486.  
  3487. VAR
  3488.     aCell:            Cell;
  3489.     listRect:        Rect;
  3490.  
  3491. BEGIN
  3492.     listRect:= sndDoc^.list^^.rView;
  3493.     listRect.right:= listRect.right + kScrollBarAdjust;
  3494.     IF PtInRect(event.where, listRect) THEN BEGIN
  3495.         IF LClick(event.where, event.modifiers, sndDoc^.list) THEN BEGIN
  3496.             aCell:= LLastClick(sndDoc^.list);
  3497.             IF PtInRect(aCell, sndDoc^.list^^.dataBounds) THEN
  3498.                 PlaySelectedSnd(sndDoc, sPlayingMsg);
  3499.         END;
  3500.         ActivateSndCntls(sndDoc);
  3501.         ListClick:= TRUE;                                            {I handled the event}
  3502.     END ELSE
  3503.         ListClick:= FALSE;
  3504. END;
  3505.  
  3506. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3507. {$S Main}
  3508. PROCEDURE DoSndDocClick(sndDoc: SndDocPeek; event: EventRecord);
  3509.  
  3510. {This is called when a mouse-down event occurs in the content of my
  3511.  application windows.  First thing to check for is a click in the list.
  3512.  If not a list click, then find what control may have been clicked in and
  3513.  handle that.
  3514.  
  3515.  VERSION 1.1: Handle the record button.}
  3516.  
  3517. VAR
  3518.     control:         ControlHandle;
  3519.  
  3520. BEGIN
  3521.     SetPort(GrafPtr(sndDoc));
  3522.     GlobalToLocal(event.where);
  3523.     IF NOT ListClick(sndDoc, event) THEN BEGIN
  3524.         IF FindControl(event.where, WindowPtr(sndDoc), control) <> 0 THEN BEGIN
  3525.             IF TrackControl(control, event.where, NIL) <> 0 THEN BEGIN
  3526.                 CASE GetControlReference(control) OF
  3527.  
  3528.                     rPlaySndCntl:
  3529.                         PlaySelectedSnd(sndDoc, sPlayingMsg);
  3530.  
  3531.                     rHyperPlayCntl:
  3532.                         PlaySelectedSnd(sndDoc, sHyperMsg);
  3533.  
  3534.                     rPlayScaleCntl:
  3535.                         PlaySndSong(sndDoc, rScaleSnd);
  3536.  
  3537.                     rMelodyCntl:
  3538.                         PlaySndSong(sndDoc, rMelodyPart1);
  3539.  
  3540.                     rStopCntl:
  3541.                         KillSound;
  3542.  
  3543.                     rRecordCntl:
  3544.                         DoRecordSound(sndDoc);
  3545.  
  3546.                 END; {CASE GetControlReference(control)}
  3547.             END; {IF TrackControl}
  3548.         END; {IF FindControl}
  3549.     END; {IF NOT ListSelect}
  3550. END; {DoSndDocClick}
  3551.  
  3552. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3553. {$S Main}
  3554. PROCEDURE DoStatClick(statWindow: StatWindowPeek; event: EventRecord);
  3555.  
  3556. {The status window has a stop button.  This will stop any sound in
  3557.  progress.  So, if the user clicks in my status window I need to check
  3558.  for this.}
  3559.  
  3560. VAR
  3561.     control:         ControlHandle;
  3562.  
  3563. BEGIN
  3564.     SetPort(GrafPtr(statWindow));
  3565.     GlobalToLocal(event.where);
  3566.     IF FindControl(event.where, WindowPtr(statWindow), control) <> 0 THEN
  3567.         IF TrackControl(control, event.where, NIL) <> 0 THEN
  3568.             KillSound;
  3569. END;
  3570.  
  3571. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3572. {$S Main}
  3573. PROCEDURE DoAboutClick(window: WindowPtr; event: EventRecord);
  3574.  
  3575. {The about window has a single OK button.  If the users clicks in it, then
  3576.  close the window.}
  3577.  
  3578. VAR
  3579.     control:         ControlHandle;
  3580.     ignore:            BOOLEAN;
  3581.  
  3582. BEGIN
  3583.     SetPort(window);
  3584.     GlobalToLocal(event.where);
  3585.     IF FindControl(event.where, WindowPtr(window), control) <> 0 THEN BEGIN
  3586.         IF TrackControl(control, event.where, NIL) <> 0 THEN
  3587.             ignore:= DoCloseWindow(window);
  3588.     END;
  3589. END;
  3590.  
  3591. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3592. {$S Main}
  3593. PROCEDURE DoUpdate(window: WindowPtr);
  3594.  
  3595. {This is called when an update event is received for any of my windows.  It
  3596.  calls the appropriate window’s update routine to draw its contents.  As an
  3597.  efficiency measure that does not have to be followed, it calls the drawing
  3598.  routine only if the visRgn is non-empty.  This will handle situations
  3599.  where calculations for drawing or drawing itself is very time-consuming.
  3600.  Why does QD give you an update with an empty updateRgn?}
  3601.  
  3602. BEGIN
  3603.     BeginUpdate(window);                                {setup the visRgn, clears updateRgn}
  3604.     IF NOT EmptyRgn(window^.visRgn) THEN BEGIN     {if updating to be done}
  3605.         SetPort(window);                                {set to the current port}
  3606.         CASE GetWRefCon(window) OF                    {call the window’s drawing routine}
  3607.  
  3608.             rSoundWindow:
  3609.                 DrawSndWindow(window);
  3610.  
  3611.             rStatusWindow:
  3612.                 DrawStatusWindow;
  3613.  
  3614.             rAboutWindow:
  3615.                 DrawAboutWindow(window);
  3616.         END;
  3617.     END;
  3618.     EndUpdate(window);                                {restores the visRgn}
  3619. END; {DoUpdate}
  3620.  
  3621. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3622. {$S Main}
  3623. PROCEDURE DoActivate(window: WindowPtr; becomingActive: BOOLEAN);
  3624.  
  3625. {This is called when a window is to be activated or deactivated.  For the
  3626.  document window I activate all the controls and the list.  For the status
  3627.  window I activate the one and only control.  This is also called for
  3628.  suspend and resume events while running MultiFinder.}
  3629.  
  3630. VAR
  3631.     kind:            INTEGER;
  3632.     button:        ControlHandle;
  3633.     r:                Rect;
  3634.  
  3635. BEGIN
  3636.     IF window <> NIL THEN BEGIN
  3637.         CASE GetWRefCon(window) OF
  3638.  
  3639.             rSoundWindow: BEGIN
  3640.                 ActivateSndCntls(SndDocPeek(window));
  3641.                 LActivate(becomingActive, SndDocPeek(window)^.list);
  3642.             END;
  3643.  
  3644.             rStatusWindow: BEGIN
  3645.                 IF becomingActive THEN                            {it only has one control}
  3646.                     HiliteControl(WindowPeek(window)^.controlList, kCntlActivate)
  3647.                 ELSE
  3648.                     HiliteControl(WindowPeek(window)^.controlList, kCntlDeactivate);
  3649.                 DoButtonOutline(WindowPeek(window)^.controlList);
  3650.             END;
  3651.  
  3652.             rAboutWindow: BEGIN
  3653.                 IF becomingActive THEN                            {it only has one control}
  3654.                     HiliteControl(WindowPeek(window)^.controlList, kCntlActivate)
  3655.                 ELSE
  3656.                     HiliteControl(WindowPeek(window)^.controlList, kCntlDeactivate);
  3657.                 DoButtonOutline(WindowPeek(window)^.controlList);
  3658.             END;
  3659.  
  3660.         END;
  3661.     END;
  3662. END; {DoActivate}
  3663.  
  3664. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3665. {$S Main}
  3666. PROCEDURE DoEvent(event: EventRecord);
  3667.  
  3668. {As Spike Lee says, “Do the right thing.” Determine what kind of event it
  3669.  is, and call the appropriate routines.  Key down events are first tested
  3670.  as being command key events for menus or command-. for cancel.  If it’s
  3671.  the command-. the user is asking to cancel the sound.  In this case, if
  3672.  the front window is the status window then the button should look as if it
  3673.  has been clicked.  This is proper human interface.  Any non-menu command
  3674.  keys are passed to DoKeyDown.  I have a global flag, gInModalState, which
  3675.  is used to handle the front window as a modal dialog.  This means I ignore
  3676.  clicks outside of the modal window and menu command keys.  I could have a
  3677.  different global flag that would mean it’s a modal window but uses menu
  3678.  commands, such as gInModalMenuState.  An important note is that I do pass
  3679.  any other non-menu command keys to DoKeys, so the modal window could have
  3680.  access to key events.  One last thing to note happens during a suspend
  3681.  event.   Applications using sound should dispose of their sound channels
  3682.  at suspend time.  No other application can use sound while another one has
  3683.  channels allocated.  To stop my sound, I call KillSound which hides the
  3684.  status window.  MultiFinder does properly deactivate the front window at
  3685.  suspend time and applications normally do not have to worry about
  3686.  HiliteWindow.  I do because the front window could have been the status
  3687.  window and I’ve removed it after getting a suspend event.  This causes the
  3688.  next window, a document window, to become highlighted as the active
  3689.  window.  To avoid my next window from being highlighted while in the
  3690.  background, I have to call HiliteWindow *after* KillSound.  This is
  3691.  strange and only applications changing the front window at suspend event
  3692.  time have to be concerned about this.
  3693.  
  3694.  VERSION 1.1: Added a constant, kSFTopLeft, to specify the dialog position.
  3695.  I do not call KillSound during a MultiFinder switch unless we're running
  3696.  the older Sound Manager.  The new one allows for multiple sound
  3697.  channels or will return the proper error otherwise.}
  3698.  
  3699. CONST
  3700. {OSEvent is the event number of the suspend/resume and mouse-moved events
  3701.  sent by MultiFinder.  Once I determine an osEvent has occurred, I look at
  3702.  the high byte of the message sent to determine which kind it is.  To
  3703.  further differentiate suspend and resume events, it is necessary to check
  3704.  the low bit of the message.  I’m using the more efficient MPW bit
  3705.  routines.}
  3706.  
  3707. {    osEvt =            app4Evt;        defined in post MPW 3.0 Events.p}
  3708. {    suspendResumeMessage = 1;    defined in post MPW 3.0 Events.p}
  3709.     kGetHighByte =    24;            {24 bits (three bytes) to shift right}
  3710.     kResumeMask =    1;                {resume event is a 1 in low bit of message}
  3711.  
  3712. VAR
  3713.     window:            WindowPtr;
  3714.     finalTicks:        LongInt;
  3715.     part, err:        INTEGER;
  3716.     ignore:            BOOLEAN;
  3717.     key:                CHAR;
  3718.  
  3719. BEGIN
  3720.     CASE event.what OF
  3721.         mouseDown: BEGIN
  3722.             part:= FindWindow(event.where, window);
  3723.             IF (IsModalWindow(FrontWindow) & (window <> FrontWindow))
  3724.              | (IsModalWindow(FrontWindow) & (part = inMenuBar)) THEN BEGIN
  3725.                 DoErrorSound(1);                            {click outside of modal window}
  3726.                 EXIT(DoEvent);                                {break out of routine}
  3727.             END;
  3728.             CASE part OF
  3729.                 inMenuBar: BEGIN                            {process the menu command}
  3730.                     AdjustMenus;
  3731.                     DoMenuCommand(MenuSelect(event.where));
  3732.                 END;
  3733.  
  3734.                 inSysWindow:                                {let system handle the mouseDown}
  3735.                     SystemClick(event, window);
  3736.  
  3737.                 inContent: BEGIN
  3738.                     IF (window <> FrontWindow) THEN
  3739.                         SelectWindow(window)
  3740.                     ELSE BEGIN
  3741.                         CASE GetWRefCon(window) OF
  3742.  
  3743.                             rSoundWindow:
  3744.                                 DoSndDocClick(SndDocPeek(window), event);
  3745.  
  3746.                             rStatusWindow:
  3747.                                 DoStatClick(StatWindowPeek(window), event);
  3748.  
  3749.                             rAboutWindow:
  3750.                                 DoAboutClick(window, event);
  3751.  
  3752.                             END; {CASE GetWRefCon(window)}
  3753.                         END;
  3754.                     END; {inContent}
  3755.  
  3756.                 inDrag:                        {pass qd.screenBits.bounds to get all gDevices}
  3757.                     DragWindow(window, event.where, qd.screenBits.bounds);
  3758.  
  3759.                 inGoAway: BEGIN
  3760.                     IF TrackGoAway(window, event.where) THEN
  3761.                         ignore:= DoCloseWindow(window);
  3762.                     END;
  3763.  
  3764.             END; {CASE}
  3765.         END; {mouseDown}
  3766.  
  3767.         keyDown, autoKey: BEGIN
  3768.             window:= FrontWindow;
  3769.             key:= CHR(BAnd(event.message, charCodeMask));
  3770.             IF BAnd(event.modifiers, cmdKey) <> 0 THEN BEGIN    {Command key down}
  3771.                 IF key = kPeriod THEN BEGIN
  3772.                     IF window = WindowPtr(gStatusWindow) THEN
  3773.                         SelectButton(WindowPeek(gStatusWindow)^.controlList);
  3774.                     KillSound;
  3775.                 END ELSE
  3776.                     IF (event.what = keyDown) & (NOT IsModalWindow(window)) THEN BEGIN
  3777.                         AdjustMenus;    {enable/disable/check menu items properly}
  3778.                         DoMenuCommand(MenuKey(key));
  3779.                     END;
  3780.             END ELSE                                                            {non-Command keys}
  3781.                 IF window <> NIL THEN                                    {if there’s a window}
  3782.                     DoKeyDown(key, window);
  3783.         END;
  3784.  
  3785.         activateEvt:                        {TRUE for activate, FALSE for deactivate}
  3786.             DoActivate(WindowPtr(event.message),
  3787.                             BAnd(event.modifiers, activeFlag) <> 0);
  3788.  
  3789.         updateEvt:                            {call DoUpdate with the window to update}
  3790.             DoUpdate(WindowPtr(event.message));
  3791.  
  3792.         diskEvt:                                {Call DIBadMount in response to a diskEvt}
  3793.             IF HiWord(event.message) <> noErr THEN BEGIN
  3794.                 err:= DIBadMount(Point(kSFTopLeft), event.message);
  3795.             END;
  3796.  
  3797.         osEvt: BEGIN
  3798.             CASE BSR(event.message, kGetHighByte) OF    {high byte of message}
  3799.  
  3800.                 suspendResumeMessage: BEGIN
  3801.                     IF BAnd(event.message, kResumeMask) <> 0 THEN
  3802.                         gInBackground:= FALSE                {it was a resume event}
  3803.                     ELSE BEGIN
  3804.                         gInBackground:= TRUE;                {it was a suspend event}
  3805.                         IF NOT HasNewSndMgr THEN
  3806.                             KillSound;                            {stop any sound}
  3807.                         window:= FrontWindow;                {get front window}
  3808.                         IF window <> NIL THEN                 {don’t use a NIL window}
  3809.                             HiliteWindow(window, FALSE);    {then properly activate it}
  3810.                     END;
  3811.                     DoActivate(FrontWindow, NOT gInBackground);
  3812.                 END; {suspendResumeMessage}
  3813.  
  3814.             END; {CASE}
  3815.         END; {osEvt}
  3816.  
  3817.     END; {CASE event.what}
  3818. END; {DoEvent}
  3819.  
  3820. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3821. {$S Main}
  3822. PROCEDURE AdjustCursor(region: RgnHandle);
  3823.  
  3824. {Change the cursor’s shape, depending on its current position.  This also
  3825.  calculates the region where the current cursor resides (for
  3826.  WaitNextEvent).  This is based on its current position in global
  3827.  coordinates.  If the mouse is ever outside of that region, an event is
  3828.  generated causing this routine to be called again by the event loop.  This
  3829.  allows me to change the region to where the mouse is currently located.
  3830.  If there is more to the event than just “the mouse moved,” this gets
  3831.  called before the event is processed to make sure the cursor is the right
  3832.  one.  In any (ahem) event, this is called again before I fall back into
  3833.  WaitNextEvent.  Extreme integer values are used to create a wide open
  3834.  region.  -INT_MAX - 1 is the largest negative integer (-32768) and INT_MAX
  3835.  is the largest positive integer (32767).
  3836.  
  3837.  BUG NOTE: The largest positive value for a region’s size is INT_MAX - 1
  3838.  due to a very old bug that still remains to this day.}
  3839.  
  3840. VAR
  3841.     window:                 WindowPtr;
  3842.     arrowRgn,
  3843.     sndCursorRgn:        RgnHandle;
  3844.     sndCursorRect:        Rect;
  3845.  
  3846. BEGIN
  3847.     window:= FrontWindow;        {I only adjust the cursor when I am in front}
  3848.     IF (NOT gInBackground) & (NOT IsDAWindow(window)) THEN BEGIN
  3849.  
  3850.         arrowRgn:= NewRgn;        {calculate regions for different cursor shapes}
  3851.         sndCursorRgn:= NewRgn;    {start with a big, big rectangular region}
  3852.         SetRectRgn(arrowRgn, -MAXINT - 1, -MAXINT - 1, MAXINT - 1, MAXINT - 1);
  3853.  
  3854.         IF IsDocWindow(window) THEN BEGIN    {calculate region for document cursor}
  3855.             sndCursorRect:= window^.portRect;
  3856.             SetPort(window);
  3857.             WITH sndCursorRect DO BEGIN        {make a global version of the viewRect}
  3858.                 LocalToGlobal(topLeft);
  3859.                 LocalToGlobal(botRight);
  3860.             END;
  3861.             RectRgn(sndCursorRgn, sndCursorRect);
  3862.             WITH window^.portBits.bounds DO
  3863.                 SetOrigin(-left, -top);
  3864.             SectRgn(sndCursorRgn, window^.visRgn, sndCursorRgn);
  3865.             SetOrigin(0, 0);
  3866.         END;
  3867.  
  3868.         DiffRgn(arrowRgn, sndCursorRgn, arrowRgn);    {subtract other region}
  3869.         IF PtInRgn(GetGlobalMouse, sndCursorRgn) THEN BEGIN
  3870.             SetCursor(GetCursor(rSndCursor)^^);         {change cursor and region}
  3871.             CopyRgn(sndCursorRgn, region);
  3872.         END ELSE BEGIN
  3873.             SetCursor(qd.arrow);
  3874.             CopyRgn(arrowRgn, region);
  3875.         END;
  3876.         DisposeRgn(arrowRgn);                {get rid of our local regions}
  3877.         DisposeRgn(sndCursorRgn);
  3878.     END;
  3879. END; {AdjustCursor}
  3880.  
  3881. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3882. {$S Main}
  3883. PROCEDURE EventLoop;
  3884.  
  3885. {Get events forever, and handle them by calling DoEvent.  Since this
  3886.  application requires System 6.0x or later I know that _WaitNextEvent is
  3887.  always there, even without MultiFinder.  MultiFinder’s sleep is used to
  3888.  determine how often I want to receive events, regardless if an event has
  3889.  actually occurred.  In this application, I don’t perform any background
  3890.  processing so I’m being super friendly by using MAXLONGINT as a sleep
  3891.  value.  This also helps keep Virtual Memory from paging me in just to run
  3892.  my event loop and find out that nothing happened.  The application will
  3893.  only be called upon for events that must be handled.  Also call
  3894.  AdjustCursor each time through the loop.  AdjustCursor will return the
  3895.  current region containing the mouse and is passed to WaitNextEvent.  If
  3896.  the mouse travels out of the region, another event is generated.  I have
  3897.  to call AdjustCursor just before doing the event to make sure the right
  3898.  cursor is shown.  Another thing, if I have a sound playing asynchronously
  3899.  then I want to dispose of my channel as soon as possible.  I set up a flag
  3900.  in the SoundUnit that will keep track of when it has a channel allocated.
  3901.  I can call the SndChanOpen function to find out if this is true.  It
  3902.  pretty much like keeping track of when you’re in the background.  If a
  3903.  channel is open, then the MultiFinder sleep time is adjusted to a
  3904.  reasonable time that will allow me to catch when the sound has completed
  3905.  so that I may dispose of my channels and status window.  That’s when I
  3906.  return kPollingSleepTime.
  3907.  
  3908.  VERSION 1.1: No longer putting the SANELib into a seperate unit, which then
  3909.  needed to be unloaded.  Instead, I merge it into the Main segment.  Refer
  3910.  to the Make file for further information.}
  3911.  
  3912.  
  3913. VAR
  3914.     cursorRgn:             RgnHandle;
  3915.     event:                 EventRecord;
  3916.     sleep:                LONGINT;
  3917.  
  3918. BEGIN
  3919.     cursorRgn:= NewRgn;                        {1st time pass WNE an empty region}
  3920.     REPEAT
  3921.         IF SndChanOpen THEN                                    {if we’re playing a sound}
  3922.             sleep:= kPollingSleepTime                        {use the polling sleep value}
  3923.         ELSE BEGIN
  3924.             sleep:= MAXLONGINT;                                {default value for sleep}
  3925.             UnloadSeg(@_SoundUnit);                            {unload the Sound Unit}
  3926.         END;
  3927.         UnloadSeg(@OpenSoundDoc);                            {unload the open code}
  3928.         IF SoundCompletion THEN
  3929.             KillSound;
  3930.         IF LowOnReserve THEN
  3931.             RecoverReserve;
  3932.         AdjustCursor(cursorRgn);                            {get the right cursor}
  3933.         IF WaitNextEvent(everyEvent, event, sleep, cursorRgn) THEN BEGIN
  3934.             AdjustCursor(cursorRgn);                        {get the right cursor}
  3935.             DoEvent(event);
  3936.         END;
  3937.     UNTIL FALSE;                    {loop forever; I quit through Terminate}
  3938. END; {EventLoop}
  3939.  
  3940.  
  3941. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  3942. {$S Initialization}
  3943. PROCEDURE Initialize;
  3944.  
  3945. {Set up the whole world.  Initialize global variables, Toolbox managers,
  3946.  and menus.  If a failure occurs here, I will consider that the application
  3947.  is in such bad shape that I should just exit.  Your error handling may
  3948.  differ, but the checks should still be made.  I ask for a set of master
  3949.  pointer blocks and all permanent storage at this point to cut down on
  3950.  memory fragmentation.  I may be opening large numbers of resources and
  3951.  documents so I allocate some extra master pointer blocks at the start.
  3952.  
  3953.  VERSION 1.1:  Now supports opening of documents from the Finder.}
  3954.  
  3955. CONST
  3956.     kBroughtToFront =    3;
  3957.  
  3958. VAR
  3959.     event:                 EventRecord;
  3960.     menuBar:             Handle;
  3961.     total, contig:        LONGINT;
  3962.     ignoreError:        OSErr;
  3963.     ignoreResult:         BOOLEAN;
  3964.     theFile:                AppFile;
  3965.     message, count,
  3966.     index:                INTEGER;
  3967.     
  3968. BEGIN
  3969.     gInBackground:= FALSE;                        {we’ll be in the foreground soon}
  3970.     gAppResRef:= CurResFile;                    {save the resRef to myself}
  3971.     FOR count:= 1 TO kNumberOfMasters DO    {allocate master pointer blocks}
  3972.         MoreMasters;
  3973.     InitGraf(@qd.thePort);                            {init managers, yawn...}
  3974.     InitFonts;
  3975.     InitWindows;
  3976.     InitMenus;
  3977.     TEInit;
  3978.     InitDialogs(NIL);
  3979.     InitCursor;
  3980.  
  3981. {This next line prevents the Dialog Manager from calling _SysBeep.
  3982.  If I get a memory or resource Manager error it wouldn’t be the best
  3983.  of plans to call  _SysBeep which will want to allocate memory and load
  3984.  a few resources.  Which could be bad if I just gave an Alert signalling
  3985.  an out of memory condition.}
  3986.  
  3987.     ErrorSound(@DoErrorSound);
  3988.  
  3989. {This code is necessary to pull the application into the foreground.  I use
  3990.  EventAvail because I don’t want to remove any events the user may have
  3991.  done, such as typing ahead.  Until the application has made a few calls (3
  3992.  seems to be the magic number) to the Event Manager, MultiFinder keeps me
  3993.  in the background.   Splashscreens and Alerts will remain in a background
  3994.  layer until we get a few events.  This is documented in Tech Note #180.}
  3995.  
  3996.     FOR count:= 1 TO kBroughtToFront DO
  3997.         ignoreResult:= EventAvail(everyEvent, event);
  3998.  
  3999. {Ignore the error returned from SysEnvirons; even if an error occurred,
  4000.  the SysEnvirons glue will fill in the SysEnvRec. You can save a redundant
  4001.  call to SysEnvirons by calling it after initializing AppleTalk.}
  4002.  
  4003.     ignoreError:= SysEnvirons(kSysEnvironsVersion, gMac);
  4004.  
  4005. {Make sure we’re running the right system.  If the Sound Manager is not
  4006.  available, then there’s no point in continuing.}
  4007.  
  4008.     IF gMac.systemVersion < kSystem602 THEN
  4009.         EmergencyExit(sWrongVersion);
  4010.  
  4011. {Call the SoundUnit to initialize itslef.  If the unit encounters an error,
  4012.  then it cannot be used and this means I’m leaving too.}
  4013.  
  4014.     IF InitSoundUnit <> noErr THEN                {allocates 4 * 1064 bytes}
  4015.         EmergencyExit(sInitSoundErr);
  4016.  
  4017. {Before I go any further, I want my reserve memory.  This is an emergency
  4018.  reserve (sorta like my old VW had) when memory runs low.  If I cannot
  4019.  obtain this reserve, then I’ll bail.  It’s also important to obtain my
  4020.  reserve before testing if I have the desired amount of memory to run
  4021.  this application.  Also, FailLowMemory will consider the memory reserve.}
  4022.  
  4023.     IF NOT AllocateReserve THEN
  4024.         EmergencyExit(sLowMemory);
  4025.     SetGrowZone(@MyGrowZone);
  4026.     
  4027.     menuBar:= GetNewMBar(rMenuBar);                {read menus into menu bar}
  4028.     IF menuBar = NIL THEN
  4029.         EmergencyExit(sNoMenus);                    {wow, how’d that happen?}
  4030.     SetMenuBar(menuBar);                                {install menus}
  4031.     DisposeHandle(menuBar);
  4032.     AppendResMenu(GetMenuHandle(mApple), 'DRVR');    {add DA names to Apple menu}
  4033.     DrawMenuBar;
  4034.  
  4035.     InitStatusWindow;                                    {get the status window ready}
  4036.     InitCursorCtl(NIL);                                {MPW’s handy cursor routines}
  4037.  
  4038. {Last, I want to make sure that enough memory is free for my application to
  4039.  run.  It is possible that user may have adjusted the SIZE resource to too
  4040.  small a setting or for some other reason the application started up in a
  4041.  very small memory partition.  It’s also possible for a situation to arise
  4042.  where the heap may have been of the requested size taken from the SIZE
  4043.  resource, but a large scrap was loaded which left too little memory.  I
  4044.  want to make sure that my free memory is not being modified by the scrap’s
  4045.  presence.  So, I unload it to disk but if the application will run once
  4046.  the scrap is unloaded, then you’ll probably not get it back into memory.
  4047.  Thus losing the clipboard contents.  I preform this check after
  4048.  initializing all the Toolbox and the basic features of this application,
  4049.  such as showing the about box.}
  4050.  
  4051.     IF FailLowMemory(kMinSpace) THEN BEGIN
  4052.         IF UnloadScrap <> noErr THEN
  4053.             EmergencyExit(sLowMemory)
  4054.         ELSE BEGIN
  4055.             IF FailLowMemory(kMinSpace) THEN
  4056.                 EmergencyExit(sLowMemory);
  4057.         END;
  4058.     END;
  4059.  
  4060. {I check the sound volume after testing for a minimal amount of memory to
  4061.  run.  Otherwise I might end up telling the user to adjust the volume and
  4062.  the quitting because memory was too low.}
  4063.  
  4064.     CheckSndVol;                                        {check the sound volume level}
  4065.     
  4066.     CountAppFiles(message, count);                {any files to open?}
  4067.     FOR index:= 1 TO count DO BEGIN                {go get 'em}
  4068.         GetAppFiles(index, theFile);
  4069.         OpenSoundDoc(@theFile.fName, theFile.vRefNum);
  4070.     END;
  4071.     ClrAppFiles(count);
  4072. END; {Initialize}
  4073.  
  4074. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  4075. PROCEDURE _DataInit; EXTERNAL;
  4076.  
  4077. {This routine is contained in the MPW runtime library.  It will be placed
  4078.  into the code segment used to initialize the A5 globals.  This external
  4079.  reference to it is done so that we can unload that segment, named %A5Init.}
  4080.  
  4081. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  4082. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROGRAM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  4083. {$S Main}
  4084. BEGIN
  4085.     UnloadSeg(@_DataInit);    {note that _DataInit must not be in Main!}
  4086.  
  4087. {If you have stack requirements that differ from the default, then you
  4088.  could use SetApplLimit to increase StackSpace at this point, before
  4089.  calling MaxApplZone.}
  4090.  
  4091.     MaxApplZone;                {expand the heap so code segments load at the top}
  4092.     Initialize;                    {initialize the program}
  4093.     UnloadSeg(@Initialize);    {note that Initialize must not be in Main!}
  4094.     EventLoop;                    {call the main event loop}
  4095. END.
  4096. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROGRAM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  4097. {~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
  4098.